Skip to content

Commit

Permalink
chore: changelog release automation (MetaMask#10172)
Browse files Browse the repository at this point in the history
## **Description**

This PR updates the release cut GH action. The action will now generate
the changelog from a CSV file of changes between to
commits/tag/branches.

The input for this action now takes in a tag from the previous release
in-order to base the changes.

The CSV file generate now uses the tag on each PR to assign the work to
team for verification.

## **Related issues**

Fixes:

## **Manual testing steps**

1. Plug this branch into the GH action
2. Confirm it generates a release branch and csv test file

## **Screenshots/Recordings**

### **Before**

Example output: MetaMask#10052

### **After**

Example output: MetaMask#10242

## **Pre-merge author checklist**

- [ ] 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).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] 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.
  • Loading branch information
sethkfman authored Jul 16, 2024
1 parent e16262f commit 0ba873a
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 17 deletions.
19 changes: 11 additions & 8 deletions .github/workflows/create-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ on:
version-number:
description: 'A natural version number. eg: 862'
required: true

previous-version-tag:
description: 'Previous release version tag. eg: v7.7.0'
required: true
jobs:
create-release-pr:
runs-on: ubuntu-latest
Expand All @@ -34,20 +36,21 @@ jobs:
# The workaround is to use a personal access token (BUG_REPORT_TOKEN) instead of
# the default GITHUB_TOKEN for the checkout action.
token: ${{ secrets.BUG_REPORT_TOKEN }}
- name: Get Node.js version
id: nvm
run: echo "NODE_VERSION=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
node-version-file: '.nvmrc'
cache: yarn
- name: Install dependencies
run: yarn --immutable
- name: Set Versions
id: set-versions
shell: bash
run: SEMVER_VERSION=${{ github.event.inputs.semver-version }} VERSION_NUMBER=${{ github.event.inputs.version-number }} yarn create-release
run: SEMVER_VERSION=${{ github.event.inputs.semver-version }} VERSION_NUMBER=${{ github.event.inputs.version-number }} yarn set-version
- name: Create Release PR
id: create-release-pr
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./scripts/create-release-pr.sh ${{ github.event.inputs.semver-version }}
./scripts/create-release-pr.sh ${{ github.event.inputs.previous-version-tag }} ${{ github.event.inputs.semver-version }}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@
"test:merge-coverage": "nyc report --temp-dir ./tests/coverage --report-dir ./tests/merged-coverage/ --reporter json --reporter text --reporter lcovonly",
"test:validate-coverage": "nyc check-coverage --nycrc-path ./coverage-thresholds.json -t ./tests/merged-coverage/",
"update-changelog": "./scripts/auto-changelog.sh",
"changeset-changelog": "wrap () { node ./scripts/generate-rc-commits.js \"$@\" && ./scripts/changelog-csv.sh }; wrap ",
"prestorybook": "rnstl",
"deduplicate": "yarn yarn-deduplicate && yarn install",
"create-release": "./scripts/set-versions.sh && yarn update-changelog",
"set-version": "./scripts/set-versions.sh",
"add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts",
"add-team-label-to-pr": "ts-node ./.github/scripts/add-team-label-to-pr.ts",
"run-bitrise-e2e-check": "ts-node ./.github/scripts/bitrise/run-bitrise-e2e-check.ts",
Expand Down Expand Up @@ -373,6 +374,7 @@
"@metamask/object-multiplex": "^1.1.0",
"@metamask/providers": "^13.1.0",
"@metamask/test-dapp": "^8.9.0",
"@octokit/rest": "^21.0.0",
"@open-rpc/mock-server": "^1.7.5",
"@open-rpc/schema-utils-js": "^1.16.2",
"@open-rpc/test-coverage": "^2.2.2",
Expand Down Expand Up @@ -479,6 +481,7 @@
"regenerator-runtime": "0.13.9",
"rn-nodeify": "10.3.0",
"serve-handler": "^6.1.5",
"simple-git": "^3.22.0",
"ts-node": "^10.5.0",
"typescript": "~4.8.4",
"wdio-cucumberjs-json-reporter": "^4.4.3",
Expand Down
80 changes: 80 additions & 0 deletions scripts/changelog-csv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash

set -e
set -u
set -o pipefail

readonly CSV_FILE='commits.csv'

# Add release branch arg name
RELEASE_BRANCH_NAME="${1}"

# Temporary file for new entries
NEW_ENTRIES=$(mktemp)

# Backup file for existing CHANGELOG
CHANGELOG="CHANGELOG.md"
CHANGELOG_BACKUP="$CHANGELOG.bak"

# Backup existing CHANGELOG.md
cp "$CHANGELOG" "$CHANGELOG_BACKUP"

# Function to append entry to the correct category in the temp file
append_entry() {
local change_type="$1"
local entry="$2"
# Ensure the "Other" category is explicitly handled
case "$change_type" in
Added|Changed|Fixed) ;;
*) change_type="Other" ;; # Categorize as "Other" if not matching predefined categories
esac
echo "$entry" >> "$NEW_ENTRIES-$change_type"
}

# Read the CSV file and append entries to temp files based on change type
while IFS=, read -r commit_message author pr_link team change_type
do
pr_id=$(echo "$pr_link" | grep -o '[^/]*$')
entry="- [#$pr_id]($pr_link): $commit_message"
append_entry "$change_type" "$entry"
done < <(tail -n +2 "$CSV_FILE") # Skip the header line

# Function to insert new entries into CHANGELOG.md after a specific line
insert_new_entries() {
local marker="## Current Main Branch"
local temp_changelog=$(mktemp)

# Find the line number of the marker
local line_num=$(grep -n "$marker" "$CHANGELOG_BACKUP" | cut -d ':' -f 1)

# Split the existing CHANGELOG at the marker line
head -n "$line_num" "$CHANGELOG_BACKUP" > "$temp_changelog"

# Append the release header
echo "" >> "$temp_changelog"
echo "## $RELEASE_BRANCH_NAME - <Date>" >> "$temp_changelog"
echo "" >> "$temp_changelog"

# Append new entries for each change type if they exist
for change_type in Added Changed Fixed Other; do
if [[ -s "$NEW_ENTRIES-$change_type" ]]; then
echo "### $change_type" >> "$temp_changelog"
cat "$NEW_ENTRIES-$change_type" >> "$temp_changelog"
echo "" >> "$temp_changelog" # Add a newline for spacing
fi
done

# Append the rest of the original CHANGELOG content
tail -n +$((line_num + 1)) "$CHANGELOG_BACKUP" >> "$temp_changelog"

# Replace the original CHANGELOG with the updated one
mv "$temp_changelog" "$CHANGELOG"
}

# Trap to ensure cleanup happens
trap 'rm -f "$NEW_ENTRIES-"* "$CHANGELOG_BACKUP"' EXIT

# Insert new entries into CHANGELOG.md
insert_new_entries

echo 'CHANGELOG updated'
14 changes: 11 additions & 3 deletions scripts/create-release-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ set -e
set -u
set -o pipefail

NEW_VERSION="${1}"
PREVIOUS_VERSION="${1}"
NEW_VERSION="${2}"
RELEASE_BRANCH_PREFIX="release/"

if [[ -z $NEW_VERSION ]]; then
Expand All @@ -13,7 +14,7 @@ if [[ -z $NEW_VERSION ]]; then
fi

RELEASE_BRANCH_NAME="${RELEASE_BRANCH_PREFIX}${NEW_VERSION}"
RELEASE_BODY="This is the release candidate for version ${NEW_VERSION}."
RELEASE_BODY="This is the release candidate for version ${NEW_VERSION}. The test plan can be found at [commit.csv](https://github.com/MetaMask/metamask-mobile/blob/${RELEASE_BRANCH_NAME}/commits.csv)"

git config user.name metamaskbot
git config user.email metamaskbot@users.noreply.github.com
Expand All @@ -30,6 +31,13 @@ git push --set-upstream origin "${RELEASE_BRANCH_NAME}"

gh pr create \
--draft \
--title "${NEW_VERSION}" \
--title "feat: ${NEW_VERSION}" \
--body "${RELEASE_BODY}" \
--head "${RELEASE_BRANCH_NAME}";

#Generate changelog and test plan csv
node ./scripts/generate-rc-commits.mjs "${PREVIOUS_VERSION}" "${RELEASE_BRANCH_NAME}"
./scripts/changelog-csv.sh "${RELEASE_BRANCH_NAME}"
git add ./commits.csv
git commit -am "updated changelog and generated feature test plan"
git push
158 changes: 158 additions & 0 deletions scripts/generate-rc-commits.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// eslint-disable-next-line import/no-nodejs-modules
import fs from 'fs';
// eslint-disable-next-line import/no-extraneous-dependencies
import simpleGit from 'simple-git';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Octokit } from '@octokit/rest';

// "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions.
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
console.log('GITHUB_TOKEN not found');
process.exit(1);
}

// Initialize Octokit with your GitHub token
const octokit = new Octokit({ auth: githubToken});

async function getPRLabels(prNumber) {
try {
const { data } = await octokit.pulls.get({
owner: 'MetaMask',
repo: 'metamask-mobile',
pull_number: prNumber[1],
});

const labels = data.labels.map(label => label.name);

// Check if any label name contains "team"
let teamArray = labels.filter(label => label.toLowerCase().startsWith('team-'));

if(teamArray.length > 1 && teamArray.includes('team-mobile-platform'))
teamArray = teamArray.filter(item => item !== 'team-mobile-platform');

return teamArray || ['Unknown'];

} catch (error) {
console.error(`Error fetching labels for PR #${prNumber}:`, error);
return ['Unknown'];
}
}

// Function to filter commits based on unique commit messages and group by teams
async function filterCommitsByTeam(branchA, branchB) {
try {
const git = simpleGit();

const logOptions = {
from: branchB,
to: branchA,
format: {
hash: '%H',
author: '%an',
message: '%s',
},
};

const log = await git.log(logOptions);
const commitsByTeam = {};

const MAX_COMMITS = 500; // Limit the number of commits to process

for (const commit of log.all) {
const { author, message, hash } = commit;
if (Object.keys(commitsByTeam).length >= MAX_COMMITS) {
console.error('Too many commits for script to work')
break;
}

// Extract PR number from the commit message using regex
const prMatch = message.match(/\(#(\d{4,5})\)$/u);
if(prMatch){
const prLink = prMatch ? `https://github.com/MetaMask/metamask-mobile/pull/${prMatch[1]}` : '';
const teams = await getPRLabels(prMatch);

// Initialize the team's commits array if it doesn't exist
if (!commitsByTeam[teams]) {
commitsByTeam[teams] = [];
}

commitsByTeam[teams].push({
message,
author,
hash: hash.substring(0, 7),
prLink,
});
}
}
return commitsByTeam;
} catch (error) {
console.error(error);
return {};
}
}

function formatAsCSV(commitsByTeam) {
const csvContent = [];
for (const [team, commits] of Object.entries(commitsByTeam)) {
commits.forEach((commit) => {
const row = [
escapeCSV(commit.message),
escapeCSV(commit.author),
commit.prLink,
escapeCSV(team),
assignChangeType(commit.message)
];
csvContent.push(row.join(','));
});
}
csvContent.unshift('Commit Message,Author,PR Link,Team,Change Type');

return csvContent;
}

// Helper function to escape CSV fields
function escapeCSV(field) {
if (field.includes(',') || field.includes('"') || field.includes('\n')) {
return `"${field.replace(/"/g, '""')}"`; // Encapsulate in double quotes and escape existing quotes
}
return field;
}
// Helper function to create change type
function assignChangeType(field) {
if (field.includes('feat'))
return 'Added';
else if (field.includes('cherry') || field.includes('bump'))
return 'Ops';
else if (field.includes('chore') || field.includes('test') || field.includes('ci') || field.includes('docs') || field.includes('refactor'))
return 'Changed';
else if (field.includes('fix'))
return 'Fixed';

return 'Unknown';
}

async function main() {
const args = process.argv.slice(2);
const fileTitle = 'commits.csv';

if (args.length !== 2) {
console.error('Usage: node generate-rc-commits.mjs branchA branchB');
process.exit(1);
}

const branchA = args[0];
const branchB = args[1];

const commitsByTeam = await filterCommitsByTeam(branchA, branchB);

if (Object.keys(commitsByTeam).length === 0) {
console.log('No commits found.');
} else {
const csvContent = formatAsCSV(commitsByTeam);
fs.writeFileSync(fileTitle, csvContent.join('\n'));
console.log('CSV file ', fileTitle, ' created successfully.');
}
}

main();
Loading

0 comments on commit 0ba873a

Please sign in to comment.