Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/build-rc-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,5 @@ jobs:
semver: ${{ needs.trigger-ios-rc-build.outputs.semantic_version }}
ios_build_number: ${{ needs.trigger-ios-rc-build.outputs.ios_version_code }}
android_build_number: ${{ needs.trigger-android-rc-build.outputs.android_version_code }}
pr_number: ${{ needs.validate-and-find-pr.outputs.pr-number }}
secrets: inherit
2 changes: 0 additions & 2 deletions .github/workflows/prod-build-env-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ jobs:
production) REMOTE_FF_ENV="prod" ;;
rc) REMOTE_FF_ENV="rc" ;;
beta) REMOTE_FF_ENV="beta" ;;
test|e2e) REMOTE_FF_ENV="test" ;;
exp) REMOTE_FF_ENV="exp" ;;
*) REMOTE_FF_ENV="dev" ;;
esac
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/slack-rc-notification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ on:
required: false
type: string
default: ''
pr_number:
description: 'PR number for linking to cherry-picks section in release PR comment'
required: false
type: string
default: ''

jobs:
slack-notification:
Expand Down Expand Up @@ -65,3 +70,4 @@ jobs:
BUILD_PIPELINE_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
ANDROID_PUBLIC_URL: ${{ secrets.ANDROID_PUBLIC_BUCKET_URL }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
PR_NUMBER: ${{ inputs.pr_number }}
113 changes: 113 additions & 0 deletions scripts/build-announce/cherry-picks-section.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Extracts commits from release branch and builds collapsible markdown section.
*/

import { execSync } from 'child_process';

const REPO_URL = process.env.GITHUB_REPOSITORY
? `https://github.com/${process.env.GITHUB_REPOSITORY}`
: 'https://github.com/MetaMask/metamask-mobile';

const SKIP_CI_BUMP_VERSION_SUBJECT = /^\[skip ci\] Bump version number to/;

function getMergeBase(headRef: string, baseRef: string): string | null {
try {
const out = execSync(`git merge-base ${headRef} ${baseRef}`, {

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
environment variable
.
This command depends on an unsanitized
environment variable
.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return out.trim() || null;
} catch {
return null;
}
}

function getAncestryPathCommits(
mergeBaseHash: string,
headRef: string,
): { hash: string; subject: string }[] {
try {
const out = execSync(
`git log --ancestry-path ${mergeBaseHash}..${headRef} --pretty=format:%h\t%s`,

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
environment variable
.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);

if (!out.trim()) return [];

return out
.trim()
.split('\n')
.filter((line) => {
const tab = line.indexOf('\t');
const subject = tab >= 0 ? line.slice(tab + 1) : line;
return !SKIP_CI_BUMP_VERSION_SUBJECT.test(subject.trim());
})
.map((line) => {
const tab = line.indexOf('\t');
return {
hash: tab >= 0 ? line.slice(0, tab) : '',
subject: tab >= 0 ? line.slice(tab + 1) : line,
};
});
} catch {
return [];
}
}

function formatSubjectWithPrLinks(subject: string): string {
return subject
.replace(/\(#(\d+)\)/g, (_, n) => `([#${n}](${REPO_URL}/pull/${n}))`)
.replace(/(^|\s)#(\d+)\b/g, (_, lead, n) => `${lead}[#${n}](${REPO_URL}/pull/${n})`);
}

export function extractCherryPicks(): { hash: string; subject: string }[] {
const baseRef = process.env.MERGE_BASE_REF ?? 'origin/main';
const headRef = (process.env.HEAD_REF ?? '').trim() || 'HEAD';

const mergeBase = getMergeBase(headRef, baseRef);
Comment thread
sleepytanya marked this conversation as resolved.
if (!mergeBase) {
console.log('[cherry-picks] Could not resolve merge-base');
return [];
}

const commits = getAncestryPathCommits(mergeBase, headRef);
console.log(`[cherry-picks] Found ${commits.length} commit(s)`);
return commits;
}

export function buildCherryPicksSection(
commits: { hash: string; subject: string }[],
): string {
if (commits.length === 0) return '';

const lines: string[] = [
'<a id="cherry-picks"></a>',
'### :cherries: What\'s in this RC\n',
'<details>',
`<summary>${commits.length} commit(s) in this release</summary>\n`,
'| Commit | Description |',
'| :--- | :--- |',
];

for (const commit of commits) {
const linkedSubject = formatSubjectWithPrLinks(commit.subject);
const commitLink = `[\`${commit.hash}\`](${REPO_URL}/commit/${commit.hash})`;
lines.push(`| ${commitLink} | ${linkedSubject} |`);
}

lines.push('\n</details>\n');
return lines.join('\n');
}

export function buildCherryPicksFailureSection(error?: string): string {
let section = `<a id="cherry-picks"></a>
### :cherries: What's in this RC

_Could not extract commit list._`;

if (error) {
section += `\n\n<details>\n<summary>Details</summary>\n\n${error}\n\n</details>`;
}

return section + '\n';
}
6 changes: 0 additions & 6 deletions scripts/build-announce/env-validation-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ function getRemoteFFEnv(env: string | undefined): string {
return 'rc';
case 'beta':
return 'beta';
case 'test':
case 'e2e':
return 'test';
case 'exp':
return 'exp';
case 'dev':
default:
return 'dev';
}
Expand Down
34 changes: 33 additions & 1 deletion scripts/build-announce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import {
buildEnvValidationSection,
buildEnvValidationFailureSection,
} from './env-validation-section';
import {
extractCherryPicks,
buildCherryPicksSection,
buildCherryPicksFailureSection,
} from './cherry-picks-section';
import { validateEnv } from './validate-env';
import type { BuildInfo, TestPlanResult, EnvValidationResult } from './types';

Expand Down Expand Up @@ -151,6 +156,10 @@ function buildCommentBody(
iosResult?: EnvValidationResult;
error?: string;
},
cherryPicks: {
commits: { hash: string; subject: string }[];
error?: string;
},
testPlanError?: string,
): string {
let body = `${RC_BUILD_COMMENT_MARKER}
Expand All @@ -171,6 +180,15 @@ ${buildMoreInfoSection(buildInfo)}
body += buildEnvValidationFailureSection(envValidation.error);
}

// Add cherry-picks section
if (cherryPicks.commits.length > 0) {
body += `---\n\n`;
body += buildCherryPicksSection(cherryPicks.commits);
} else if (cherryPicks.error) {
body += `---\n\n`;
body += buildCherryPicksFailureSection(cherryPicks.error);
}

// Add test plan section
if (testPlan) {
body += `---\n\n`;
Expand Down Expand Up @@ -261,8 +279,22 @@ async function main(): Promise<void> {
console.log(' - No build-env artifacts found');
}

// Extract cherry-picks from git history
console.log('\n=== Cherry-picks ===\n');
const cherryPicks: { commits: { hash: string; subject: string }[]; error?: string } = {
commits: [],
};

try {
cherryPicks.commits = extractCherryPicks();
console.log(` - Found ${cherryPicks.commits.length} commit(s)`);
} catch (error) {
cherryPicks.error = error instanceof Error ? error.message : String(error);
console.error(` - Error: ${cherryPicks.error}`);
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Build the comment body
const commentBody = buildCommentBody(buildInfo, testPlan, envValidation, testPlanError);
const commentBody = buildCommentBody(buildInfo, testPlan, envValidation, cherryPicks, testPlanError);

// Post comment and minimize old ones
console.log(`\n=== Posting Comment to PR #${prNumber} ===\n`);
Expand Down
24 changes: 11 additions & 13 deletions scripts/slack-rc-notification.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,7 @@ function buildSlackMessage(options) {
androidUrl,
iosUrl,
pipelineUrl,
rcCommitsText,
hasRcCommits,
prNumber,
} = options;

const blocks = [
Expand Down Expand Up @@ -269,8 +268,9 @@ function buildSlackMessage(options) {
},
];

// Add RC commit list if we have entries
if (hasRcCommits && rcCommitsText) {
// Add link to cherry-picks section in PR comment
if (prNumber) {
const cherryPicksLink = `<${REPO_URL}/pull/${prNumber}#cherry-picks|View what's in this RC>`;
blocks.push(
{
type: 'divider',
Expand All @@ -279,20 +279,17 @@ function buildSlackMessage(options) {
type: 'section',
text: {
type: 'mrkdwn',
text: `*📋 What's in this RC:*\n${rcCommitsText}`,
text: `*📋 What's in this RC:*\n${cherryPicksLink}`,
},
},
);
} else {
const releaseNotesMrkdwn = `<${REPO_URL}/tree/release/${version}|View release notes>`;
const fallbackMrkdwn = pipelineUrl
? `_Could not list RC commits (empty ancestry path, merge-base failed, incomplete git history, or \`git log\` failed). For full notes see ${releaseNotesMrkdwn} — also linked in the footer below._`
: `_Could not list RC commits (empty ancestry path, merge-base failed, incomplete git history, or \`git log\` failed). For full notes see ${releaseNotesMrkdwn}._`;
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: fallbackMrkdwn,
text: `_Cherry-picks list available in the release PR. ${releaseNotesMrkdwn}_`,
},
});
}
Expand Down Expand Up @@ -399,17 +396,19 @@ async function main() {
const pipelineUrl = process.env.BUILD_PIPELINE_URL;
const botToken = process.env.SLACK_BOT_TOKEN;

const prNumber = process.env.PR_NUMBER || '';
const expectedChannelName = getSlackChannel(version);

console.log(`\n📣 Preparing Slack notification for RC v${version} (${buildNumber})`);
if (prNumber) {
console.log(`📍 Release PR: #${prNumber}`);
}
if (isDryRun) {
console.log('🧪 DRY RUN: will print payload JSON and not call Slack');
} else {
console.log(`📍 Target channel: ${expectedChannelName}`);
}

const { text: rcCommitsText, hasEntries: hasRcCommits } = extractRcCommitsFromGit();

// Build and send the message
console.log('\n📤 Posting to Slack...');

Expand All @@ -419,8 +418,7 @@ async function main() {
androidUrl,
iosUrl,
pipelineUrl,
rcCommitsText,
hasRcCommits,
prNumber,
Comment thread
cursor[bot] marked this conversation as resolved.
});

if (isDryRun) {
Expand Down
Loading