Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d5c916f
refactor(ramp): migrate aggregator BuildQuote to HeaderStandard (#31519)
weitingsun Jun 11, 2026
4dc02bc
fix: change 'Money balance' label to 'Money account' on perps and pre…
jpuri Jun 11, 2026
d7023ae
fix(ramp): fiat order details navigation from activity and BuildQuote…
amitabh94 Jun 11, 2026
b414875
chore(rewards): UI audit (#31503)
sophieqgu Jun 11, 2026
beaffba
fix: remove max options from money deposit pages (#31532)
jpuri Jun 11, 2026
d4c14d8
feat(ci): add per-directive blocking flag to PR template checks (MCWP…
NicolasMassart Jun 11, 2026
f92a4b9
feat: mobile show all trades in the trade list (#31515)
Bigshmow Jun 11, 2026
2ef79d7
fix(ci): narrow validator return type to exclude blocking field (#31542)
NicolasMassart Jun 11, 2026
95bad08
docs(perps): update MetaMetrics reference with missing events and pro…
michalconsensys Jun 11, 2026
6087486
feat(rewards): gate VIP features behind feature flag (#31531)
VGR-GIT Jun 11, 2026
a08fede
feat(quickbuy): red/green trade toggle, slider grip haptics, and Figm…
joaosantos15 Jun 11, 2026
0a54906
fix: make Select Quote bottom sheet scrollable when 4+ quotes are sho…
zone-live Jun 11, 2026
5995ac5
fix: resolve same-chain Solana post-trade status (#31539)
bfullam Jun 11, 2026
0dd6294
feat(money): update send action icon and copy, align add/send funds s…
Kureev Jun 11, 2026
fd1d5ad
fix(money): route home-card Add/Earn CTA to direct mUSD deposit (MUSD…
Kureev Jun 11, 2026
7be2ddd
test: Sync Feature Flag Registry - 2026-06-02 01:57 UTC (#30909)
github-actions[bot] Jun 11, 2026
782493c
fix: token selection in withdraw money account page (#31535)
jpuri Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/pull-request-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ markdown and must NOT be removed or edited without updating the validator regist
type=manual-testing Section must have real testing steps or an explicit N/A.
type=screenshot Section must have evidence (image/URL) or an explicit N/A.
type=checklist Section must have all checkboxes consciously checked.
required=true|false Whether a missing/invalid section blocks the PR check.
required=true|false Whether a missing/invalid section runs the validator at all.
blocking=true|false Whether a failure of this check fails the CI workflow.
Default: false — failures are shown as warnings in the sticky
comment but do not block the PR.

Sections without a directive are checked for structural presence only.
-->
Expand All @@ -36,7 +39,7 @@ Write a short description of the changes included in this pull request, also inc

## **Changelog**

<!-- mms-check: type=changelog required=true -->
<!-- mms-check: type=changelog required=true blocking=true -->

<!--
If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either:
Expand Down
132 changes: 132 additions & 0 deletions .github/scripts/check-template-and-add-labels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,137 @@ describe('check-template-and-add-labels — base-branch guard', () => {
// runAllChecks being called proves the base-branch guard did not short-circuit.
expect(mockRunAllChecks).toHaveBeenCalled();
});

it('warning-only failures: exits 0, posts sticky comment, removes label, calls core.warning not setFailed', async () => {
process.env.LABEL_TOKEN = 'test-token';
mockContextPayload.pull_request = {
number: 42,
base: { ref: 'main' },
draft: false,
};
mockRunAllChecks.mockReturnValue([
{ ok: false, reason: 'Description is empty.', blocking: false },
]);

await loadModule();

// With babel's transform-inline-environment-variables, process.env reads are
// compiled as their build-time values (undefined for LABEL_TOKEN in CI). The
// LABEL_TOKEN guard at the top of main() always calls setFailed('LABEL_TOKEN
// not found'), but code falls through (no early return there) to the actual
// check logic. We therefore assert that setFailed was NOT called with a blocking
// issue reason (which is what the warning path must never produce).
expect(mockSetFailed).not.toHaveBeenCalledWith(
expect.stringContaining('blocking issues'),
);
expect(mockCoreWarning).toHaveBeenCalledWith(
expect.stringContaining('Description is empty.'),
);
// Label removed because all section headings are present (template mock matches everything).
expect(mockRemoveLabelFromLabelableIfPresent).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ name: 'INVALID-PR-TEMPLATE' }),
);
// Sticky comment is posted with a non-null body (renderFailureComment is mocked).
expect(mockUpsertStickyComment).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.any(String),
);
expect(processExitSpy).toHaveBeenCalledWith(0);
});

it('blocking failure: exits 1, posts sticky comment, removes label, calls setFailed not warning', async () => {
process.env.LABEL_TOKEN = 'test-token';
mockContextPayload.pull_request = {
number: 42,
base: { ref: 'main' },
draft: false,
};
mockRunAllChecks.mockReturnValue([
{ ok: false, reason: 'Changelog section is missing.', blocking: true },
]);

await loadModule();

expect(mockSetFailed).toHaveBeenCalledWith(
expect.stringContaining('Changelog section is missing.'),
);
expect(mockCoreWarning).not.toHaveBeenCalled();
// Label removed because all section headings are present.
expect(mockRemoveLabelFromLabelableIfPresent).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ name: 'INVALID-PR-TEMPLATE' }),
);
// Sticky comment is posted with a non-null body.
expect(mockUpsertStickyComment).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.any(String),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('mixed blocking+warning failures: exits 1 driven by the blocking failure', async () => {
process.env.LABEL_TOKEN = 'test-token';
mockContextPayload.pull_request = {
number: 42,
base: { ref: 'main' },
draft: false,
};
mockRunAllChecks.mockReturnValue([
{ ok: false, reason: 'Changelog missing.', blocking: true },
{ ok: false, reason: 'Description empty.', blocking: false },
]);

await loadModule();

expect(mockSetFailed).toHaveBeenCalledWith(
expect.stringContaining('Changelog missing.'),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('draft PR with blocking failure: exits 1 (same rules as ready-for-review)', async () => {
process.env.LABEL_TOKEN = 'test-token';
mockContextPayload.pull_request = {
number: 42,
base: { ref: 'main' },
draft: true,
};
mockRunAllChecks.mockReturnValue([
{ ok: false, reason: 'Changelog missing.', blocking: true },
]);

await loadModule();

expect(mockSetFailed).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('draft PR with warning-only failures: exits 0 (informational, same as ready-for-review)', async () => {
process.env.LABEL_TOKEN = 'test-token';
mockContextPayload.pull_request = {
number: 42,
base: { ref: 'main' },
draft: true,
};
mockRunAllChecks.mockReturnValue([
{ ok: false, reason: 'Description empty.', blocking: false },
]);

await loadModule();

// Same comment as warning-only test above: LABEL_TOKEN is inlined as undefined
// so setFailed('LABEL_TOKEN not found') fires first, but code falls through.
// Assert the workflow-blocking path is NOT triggered.
expect(mockSetFailed).not.toHaveBeenCalledWith(
expect.stringContaining('blocking issues'),
);
expect(mockCoreWarning).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(0);
});
});
});
59 changes: 32 additions & 27 deletions .github/scripts/check-template-and-add-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,57 +184,62 @@ async function main(): Promise<void> {
process.exit(0);
}

// Draft PRs see the same checks but with informational status only, so
// authors can preview what to fix before flipping to "Ready for review".
// The check name stays identical in both states; branch protection can
// require it without extra wiring.
const isDraft = Boolean(context.payload.pull_request?.draft);
const hasNoChangelogLabel = labelable.labels?.some(
(label) => label.name === 'no-changelog',
);

const failures: { ok: false; reason: string }[] = [];
// Section-heading check: every expected `## **…**` body heading must be present.
// The INVALID-PR-TEMPLATE label tracks this check only (pre-#30541 semantics) —
// semantic failures surface through the sticky comment and exit status instead.
const sectionHeadingsMissing = templateType !== TemplateType.PullRequest;

if (templateType !== TemplateType.PullRequest) {
if (sectionHeadingsMissing) {
await addLabelToLabelable(octokit, labelable, invalidPullRequestTemplateLabel);
} else {
await removeLabelFromLabelableIfPresent(octokit, labelable, invalidPullRequestTemplateLabel);
}

const failures: { ok: false; reason: string; blocking: boolean }[] = [];

if (sectionHeadingsMissing) {
// Section-heading mismatch is informational (non-blocking), matching pre-#30541 behavior.
failures.push({
ok: false,
reason:
'PR body does not match `pull-request-template.md` (one or more section titles are missing). See https://github.com/MetaMask/metamask-mobile/blob/main/.github/scripts/shared/pr-template-checks.ts#L15-L23.',
'PR body does not match `pull-request-template.md` (one or more section headings are missing). See https://github.com/MetaMask/metamask-mobile/blob/main/.github/scripts/shared/pr-template-checks.ts#L15-L23.',
blocking: false,
});
} else {
failures.push(...runAllChecks(labelable.body, Boolean(hasNoChangelogLabel)));
}

if (failures.length > 0) {
const bullets = failures.map((f) => `- ${f.reason}`).join('\n');
await addLabelToLabelable(
octokit,
labelable,
invalidPullRequestTemplateLabel,
);
const blockingFailures = failures.filter((f) => f.blocking);
const warningFailures = failures.filter((f) => !f.blocking);

await upsertStickyComment(
octokit,
labelable,
renderFailureComment(failures.map((f) => f.reason), isDraft),
renderFailureComment({
blocking: blockingFailures.map((f) => f.reason),
warning: warningFailures.map((f) => f.reason),
}),
);

if (isDraft) {
core.warning(
`PR template is not yet ready for review:\n${bullets}\n\nThis check is informational while the PR is in draft.`,
);
process.exit(0);
if (blockingFailures.length > 0) {
const bullets = blockingFailures.map((f) => `- ${f.reason}`).join('\n');
core.setFailed(`PR template has blocking issues:\n${bullets}`);
process.exit(1);
return;
}

core.setFailed(`PR template is not ready for review:\n${bullets}`);
process.exit(1);
const bullets = warningFailures.map((f) => `- ${f.reason}`).join('\n');
core.warning(`PR template has warnings (informational, does not block merging):\n${bullets}`);
process.exit(0);
return;
}

console.log("PR matches 'pull-request-template.md' template and is materially complete.");
await removeLabelFromLabelableIfPresent(
octokit,
labelable,
invalidPullRequestTemplateLabel,
);
await upsertStickyComment(octokit, labelable, null);
} else {
core.setFailed(
Expand Down
Loading
Loading