Skip to content

Commit 7e1cc53

Browse files
authored
feat: add risk label to PRs from Smart E2E selection output (#27474)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> - Surfaces the `riskLevel` output (`low` / `medium` / `high`) from the Smart E2E AI analyzer as a GitHub Actions output (`ai_risk_level`) on the `smart-e2e-selection` action - Adds a new standalone script (`e2e-risk-label.mjs`) that applies a `risk-low`, `risk-medium`, or `risk-high` label to the PR based on that output - Stale risk labels are removed before the new one is applied, so re-runs always reflect the latest assessment - When `force_run=true` (i.e. `skip-smart-e2e-selection` label is present), risk is pinned to `high` - Adds `issues: write` permission to the `smart-e2e-selection` CI job to allow repo-level label creation ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [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](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes CI workflow permissions and adds GitHub API automation that can fail or mislabel PRs, affecting the PR workflow but not production code. > > **Overview** > Surfaces the AI analyzer’s `riskLevel` as a new `ai_risk_level` output from the `smart-e2e-selection` composite action (with `force_run=true` overriding the value to `high`). > > Adds a new script, `e2e-risk-label.mjs`, that ensures `risk-low`/`risk-medium`/`risk-high` labels exist, removes any stale risk label from the PR, and applies the current one. > > Updates the `ci.yml` `smart-e2e-selection` job to grant `issues: write` and to sparse-checkout the new script so the job can create and manage labels. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0fd7dbf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 3bdf001 commit 7e1cc53

4 files changed

Lines changed: 137 additions & 1 deletion

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ outputs:
4141
ai_confidence:
4242
description: 'AI confidence score (0-100)'
4343
value: ${{ steps.final-outputs.outputs.ai_confidence }}
44+
ai_risk_level:
45+
description: 'Risk level of the PR (low, medium, high) — indicates testing need and bug introduction likelihood'
46+
value: ${{ steps.final-outputs.outputs.ai_risk_level }}
4447
ai_performance_test_tags:
4548
description: 'Performance test tags to run (JSON array format, empty [] means no performance tests)'
4649
value: ${{ steps.final-outputs.outputs.ai_performance_test_tags }}
@@ -188,6 +191,15 @@ runs:
188191
else
189192
echo "force_run=false" >> "$GITHUB_OUTPUT"
190193
fi
194+
# Risk level: force_run → always high; otherwise use AI output
195+
AI_RISK='${{ steps.ai-analysis.outputs.ai_risk_level }}'
196+
if [[ "$FORCE_RUN" == "true" ]]; then
197+
echo "ai_risk_level=high" >> "$GITHUB_OUTPUT"
198+
elif [[ -n "$AI_RISK" ]]; then
199+
echo "ai_risk_level=$AI_RISK" >> "$GITHUB_OUTPUT"
200+
else
201+
echo "ai_risk_level=" >> "$GITHUB_OUTPUT"
202+
fi
191203
192204
- name: Display AI Analysis Outputs
193205
if: always()
@@ -197,6 +209,7 @@ runs:
197209
echo "================================"
198210
echo "ai_e2e_test_tags: ${{ steps.final-outputs.outputs.ai_e2e_test_tags }}"
199211
echo "ai_confidence: ${{ steps.final-outputs.outputs.ai_confidence }}"
212+
echo "ai_risk_level: ${{ steps.final-outputs.outputs.ai_risk_level }}"
200213
echo "ai_performance_test_tags: ${{ steps.final-outputs.outputs.ai_performance_test_tags }}"
201214
echo "force_run: ${{ steps.final-outputs.outputs.force_run }}"
202215
echo "================================"
@@ -231,6 +244,16 @@ runs:
231244
echo "📝 No Smart E2E selection comments found"
232245
fi
233246
247+
- name: Apply risk label to PR
248+
if: inputs.pr-number != '' && inputs.github-token != ''
249+
shell: bash
250+
env:
251+
GH_TOKEN: ${{ inputs.github-token }}
252+
GITHUB_REPOSITORY: ${{ inputs.repository }}
253+
PR_NUMBER: ${{ inputs.pr-number }}
254+
RISK_LEVEL: ${{ steps.final-outputs.outputs.ai_risk_level }}
255+
run: node .github/scripts/e2e-risk-label.mjs
256+
234257
- name: Create PR comment
235258
if: inputs.post-comment == 'true' && inputs.pr-number != '' && inputs.github-token != ''
236259
shell: bash

.github/scripts/e2e-risk-label.mjs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Applies a risk label (risk-low / risk-medium / risk-high) to a PR based on
5+
* the risk level output from the Smart E2E selection step.
6+
*
7+
* Required environment variables:
8+
* RISK_LEVEL - 'low' | 'medium' | 'high'
9+
* GH_TOKEN - GitHub token with pull-requests:write and issues:write
10+
* GITHUB_REPOSITORY - owner/repo
11+
* PR_NUMBER - pull request number
12+
*/
13+
14+
const RISK_LEVEL = process.env.RISK_LEVEL || '';
15+
const GH_TOKEN = process.env.GH_TOKEN || '';
16+
const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY || '';
17+
const PR_NUMBER = process.env.PR_NUMBER || '';
18+
19+
const RISK_LABELS = {
20+
low: {
21+
color: '0E8A16',
22+
description: 'Low testing needed · Low bug introduction risk',
23+
},
24+
medium: {
25+
color: 'FBCA04',
26+
description: 'Moderate testing recommended · Possible bug introduction risk',
27+
},
28+
high: {
29+
color: 'B60205',
30+
description: 'Extensive testing required · High bug introduction risk',
31+
},
32+
};
33+
34+
async function githubApi(path, options = {}) {
35+
const res = await fetch(`https://api.github.com${path}`, {
36+
...options,
37+
headers: {
38+
Authorization: `Bearer ${GH_TOKEN}`,
39+
Accept: 'application/vnd.github+json',
40+
'X-GitHub-Api-Version': '2022-11-28',
41+
'Content-Type': 'application/json',
42+
...options.headers,
43+
},
44+
});
45+
return res;
46+
}
47+
48+
async function main() {
49+
if (!RISK_LEVEL) {
50+
console.log('⏭️ No risk level provided, skipping label');
51+
return;
52+
}
53+
54+
if (!RISK_LABELS[RISK_LEVEL]) {
55+
console.error(`❌ Unknown risk level: "${RISK_LEVEL}"`);
56+
process.exit(1);
57+
}
58+
59+
if (!GH_TOKEN || !GITHUB_REPOSITORY || !PR_NUMBER) {
60+
console.error('❌ Missing required env: GH_TOKEN, GITHUB_REPOSITORY, PR_NUMBER');
61+
process.exit(1);
62+
}
63+
64+
// Ensure all three risk labels exist on the repo (idempotent — 422 = already exists)
65+
for (const [level, meta] of Object.entries(RISK_LABELS)) {
66+
await githubApi(`/repos/${GITHUB_REPOSITORY}/labels`, {
67+
method: 'POST',
68+
body: JSON.stringify({ name: `risk-${level}`, color: meta.color, description: meta.description }),
69+
});
70+
}
71+
72+
// Fetch current PR labels
73+
const labelsRes = await githubApi(`/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels`);
74+
if (!labelsRes.ok) {
75+
const body = await labelsRes.text();
76+
console.error(`❌ Failed to fetch PR labels: ${labelsRes.status} ${body}`);
77+
process.exit(1);
78+
}
79+
const currentLabels = await labelsRes.json();
80+
81+
// Remove stale risk labels
82+
for (const label of currentLabels) {
83+
if (Object.keys(RISK_LABELS).map(l => `risk-${l}`).includes(label.name)) {
84+
await githubApi(`/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels/${encodeURIComponent(label.name)}`, {
85+
method: 'DELETE',
86+
});
87+
console.log(`🗑️ Removed stale label: ${label.name}`);
88+
}
89+
}
90+
91+
// Add the new risk label
92+
const newLabel = `risk-${RISK_LEVEL}`;
93+
const addRes = await githubApi(`/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels`, {
94+
method: 'POST',
95+
body: JSON.stringify({ labels: [newLabel] }),
96+
});
97+
98+
if (!addRes.ok) {
99+
const body = await addRes.text();
100+
console.error(`❌ Failed to add label "${newLabel}": ${addRes.status} ${body}`);
101+
process.exit(1);
102+
}
103+
104+
console.log(`✅ Applied risk label: ${newLabel}`);
105+
}
106+
107+
main().catch(error => {
108+
console.error('❌ Unexpected error:', error);
109+
process.exit(1);
110+
});

.github/scripts/e2e-smart-selection.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ function generatePRComment(summaryContent) {
6262
}
6363

6464
function setGitHubOutputs(analysis) {
65-
const { tags, confidence, performanceTests } = analysis;
65+
const { tags, confidence, riskLevel, performanceTests } = analysis;
6666
setGithubOutputs('ai_e2e_test_tags', tags);
6767
setGithubOutputs('ai_confidence', confidence);
68+
setGithubOutputs('ai_risk_level', riskLevel);
6869
// Performance test tags (empty array means no performance tests needed)
6970
setGithubOutputs('ai_performance_test_tags', JSON.stringify(performanceTests.selectedTags));
7071
}

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ jobs:
408408
continue-on-error: true
409409
permissions:
410410
contents: read
411+
issues: write
411412
pull-requests: write
412413
outputs:
413414
ai_e2e_test_tags: ${{ steps.e2e-selection.outputs.ai_e2e_test_tags }}
@@ -419,6 +420,7 @@ jobs:
419420
with:
420421
sparse-checkout: |
421422
.github/actions/smart-e2e-selection
423+
.github/scripts/e2e-risk-label.mjs
422424
sparse-checkout-cone-mode: false
423425
fetch-depth: 1
424426

0 commit comments

Comments
 (0)