Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update getPullRequestsMergedBetween logic #6906

Merged
merged 26 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
95a1967
Update getPullRequestsMergedBetween logic
roryabraham Dec 27, 2021
3be3312
Trim output before wrapping with brackets
roryabraham Dec 27, 2021
18f4319
Ignore PRs on update-staging-from-main and update-production-from-sta…
roryabraham Dec 28, 2021
97f1a26
Get rid of --merges flag because it does not work for CP'd merges
roryabraham Dec 28, 2021
a1d27a2
Rebuild GH actions
roryabraham Dec 28, 2021
924c87a
Start setting up automated test
roryabraham Jan 5, 2022
b59d28d
Sanitize subjects to remove double quotes
roryabraham Jan 5, 2022
2aa5460
Simplify output to only include pull request numbers
roryabraham Jan 5, 2022
ed15396
Complete scenario 1
roryabraham Jan 5, 2022
26a56a1
Add scenario 2
roryabraham Jan 5, 2022
1b32190
Save draft state with CP not quite working
roryabraham Jan 5, 2022
822b5a6
Fix CP commit hashes
roryabraham Jan 5, 2022
0682b01
Finish writing tests
roryabraham Jan 5, 2022
9fab999
Add tests to GitHub actions
roryabraham Jan 5, 2022
7ec7a51
Add git configuration
roryabraham Jan 5, 2022
343f860
Rebuild GH actions
roryabraham Jan 5, 2022
d254711
Make git config global
roryabraham Jan 5, 2022
7bb8b5b
Get rid of local abbreviated git commands
roryabraham Jan 5, 2022
97c9825
Add echo for dummy dir
roryabraham Jan 5, 2022
dc57dd2
Use HOME environment variable to make dummy repo
roryabraham Jan 6, 2022
d4a3484
Try disabling commit signing in the dummy repo
roryabraham Jan 6, 2022
48ccd30
Make OSBotify actually sign commits
roryabraham Jan 6, 2022
7c2d728
Merge branch 'main' into Rory-FixCPDeployComment
roryabraham Jan 6, 2022
e7d9e29
Add back global flag for all git configs
roryabraham Jan 6, 2022
4a96970
Update return type
roryabraham Jan 7, 2022
824d952
Add comment for sanitized output
roryabraham Jan 7, 2022
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
85 changes: 63 additions & 22 deletions .github/actions/createOrUpdateStagingDeploy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,43 +188,84 @@ const _ = __nccwpck_require__(3571);
const {execSync} = __nccwpck_require__(3129);

/**
* Takes in two git refs and returns a list of PR numbers of all PRs merged between those two refs
* Get merge logs between two refs (inclusive) as a JavaScript object.
*
* @param {String} fromRef
* @param {String} toRef
* @returns {Array}
* @returns {Object<{commit: String, subject: String}>}
*/
function getPullRequestsMergedBetween(fromRef, toRef) {
const command = `git log --format="{[%B]}" ${fromRef}...${toRef}`;
function getMergeLogsAsJSON(fromRef, toRef) {
const command = `git log --format='{"commit": "%H", "subject": "%s"},' ${fromRef}^...${toRef}`;
console.log('Getting pull requests merged between the following refs:', fromRef, toRef);
console.log('Running command: ', command);
const localGitLogs = execSync(command).toString();
const result = execSync(command).toString().trim();

// Parse the git log into an array of commit messages between the two refs
const commitMessages = _.map(
[...localGitLogs.matchAll(/{\[([\s\S]*?)\]}/gm)],
match => match[1],
);
console.log(`A list of commits made between ${fromRef} and ${toRef}:\n${commitMessages}`);
// Remove any double-quotes from commit subjects
const sanitizedOutput = result
.replace(/(?<="subject": ").*(?="})/g, subject => subject.replace(/"/g, "'"));

// We need to find which commit messages correspond to merge commits and get PR numbers.
// Additionally, we omit merge commits made while cherry picking using negative lookahead in the regexp.
const pullRequestIDs = _.reduce(commitMessages, (mergedPRs, commitMessage) => {
const mergeCommits = [
...commitMessage.matchAll(/Merge pull request #(\d{1,6}) from (?!(?:Expensify\/(?:master|main|version-))|(?:([\s\S]*?)\(cherry picked from commit .*\)\s*))/gm),
];
// Then format as JSON and convert to a proper JS object
const json = `[${sanitizedOutput}]`.replace('},]', '}]');
return JSON.parse(json);
}

/**
* Parse merged PRs, excluding those from irrelevant branches.
*
* @param {Array<String>} commitMessages
* @returns {Array<String>}
*/
function getValidMergedPRs(commitMessages) {
return _.reduce(commitMessages, (mergedPRs, commitMessage) => {
if (!_.isString(commitMessage)) {
return mergedPRs;
}

// Get the PR number of the first match (there should not be multiple matches in one commit message)
if (_.size(mergeCommits)) {
mergedPRs.push(mergeCommits[0][1]);
const match = commitMessage.match(/Merge pull request #(\d+) from (?!Expensify\/(?:master|main|version-|update-staging-from-main|update-production-from-staging))/);
if (!_.isNull(match) && match[1]) {
mergedPRs.push(match[1]);
}

return mergedPRs;
}, []);
console.log(`A list of pull requests merged between ${fromRef} and ${toRef}:\n${pullRequestIDs}`);
return pullRequestIDs;
}

/**
* Takes in two git refs and returns a list of PR numbers of all PRs merged between those two refs
*
* @param {String} fromRef
* @param {String} toRef
* @returns {Array<String>} – Pull request numbers
*/
function getPullRequestsMergedBetween(fromRef, toRef) {
const targetMergeList = getMergeLogsAsJSON(fromRef, toRef);
console.log(`Commits made between ${fromRef} and ${toRef}:`, targetMergeList);

// Get the full history on this branch, inclusive of the oldest commit from our target comparison
const oldestCommit = _.last(targetMergeList).commit;
const fullMergeList = getMergeLogsAsJSON(oldestCommit, 'HEAD');

// Remove from the final merge list any commits whose message appears in the full merge list more than once.
// This indicates that the PR should not be included in our list because it is a duplicate, and thus has already been processed by our CI
// See https://github.com/Expensify/App/issues/4977 for details
const duplicateMergeList = _.chain(fullMergeList)
.groupBy('subject')
.values()
.filter(i => i.length > 1)
.flatten()
.pluck('commit')
.value();
const finalMergeList = _.filter(targetMergeList, i => !_.contains(duplicateMergeList, i.commit));
console.log('Filtered out the following commits which were duplicated in the full git log:', _.difference(targetMergeList, finalMergeList));

// Find which commit messages correspond to merged PR's
const pullRequestNumbers = getValidMergedPRs(_.pluck(finalMergeList, 'subject'));
console.log(`List of pull requests merged between ${fromRef} and ${toRef}`, pullRequestNumbers);
return pullRequestNumbers;
}

module.exports = {
getValidMergedPRs,
getPullRequestsMergedBetween,
};

Expand Down
89 changes: 65 additions & 24 deletions .github/actions/getDeployPullRequestList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,43 +110,84 @@ const _ = __nccwpck_require__(3571);
const {execSync} = __nccwpck_require__(3129);

/**
* Takes in two git refs and returns a list of PR numbers of all PRs merged between those two refs
* Get merge logs between two refs (inclusive) as a JavaScript object.
*
* @param {String} fromRef
* @param {String} toRef
* @returns {Array}
* @returns {Object<{commit: String, subject: String}>}
*/
function getPullRequestsMergedBetween(fromRef, toRef) {
const command = `git log --format="{[%B]}" ${fromRef}...${toRef}`;
function getMergeLogsAsJSON(fromRef, toRef) {
const command = `git log --format='{"commit": "%H", "subject": "%s"},' ${fromRef}^...${toRef}`;
console.log('Getting pull requests merged between the following refs:', fromRef, toRef);
console.log('Running command: ', command);
const localGitLogs = execSync(command).toString();
const result = execSync(command).toString().trim();

// Parse the git log into an array of commit messages between the two refs
const commitMessages = _.map(
[...localGitLogs.matchAll(/{\[([\s\S]*?)\]}/gm)],
match => match[1],
);
console.log(`A list of commits made between ${fromRef} and ${toRef}:\n${commitMessages}`);

// We need to find which commit messages correspond to merge commits and get PR numbers.
// Additionally, we omit merge commits made while cherry picking using negative lookahead in the regexp.
const pullRequestIDs = _.reduce(commitMessages, (mergedPRs, commitMessage) => {
const mergeCommits = [
...commitMessage.matchAll(/Merge pull request #(\d{1,6}) from (?!(?:Expensify\/(?:master|main|version-))|(?:([\s\S]*?)\(cherry picked from commit .*\)\s*))/gm),
];

// Get the PR number of the first match (there should not be multiple matches in one commit message)
if (_.size(mergeCommits)) {
mergedPRs.push(mergeCommits[0][1]);
// Remove any double-quotes from commit subjects
const sanitizedOutput = result
.replace(/(?<="subject": ").*(?="})/g, subject => subject.replace(/"/g, "'"));

// Then format as JSON and convert to a proper JS object
const json = `[${sanitizedOutput}]`.replace('},]', '}]');
return JSON.parse(json);
}

/**
* Parse merged PRs, excluding those from irrelevant branches.
*
* @param {Array<String>} commitMessages
* @returns {Array<String>}
*/
function getValidMergedPRs(commitMessages) {
return _.reduce(commitMessages, (mergedPRs, commitMessage) => {
if (!_.isString(commitMessage)) {
return mergedPRs;
}

const match = commitMessage.match(/Merge pull request #(\d+) from (?!Expensify\/(?:master|main|version-|update-staging-from-main|update-production-from-staging))/);
if (!_.isNull(match) && match[1]) {
mergedPRs.push(match[1]);
}

return mergedPRs;
}, []);
console.log(`A list of pull requests merged between ${fromRef} and ${toRef}:\n${pullRequestIDs}`);
return pullRequestIDs;
}

/**
* Takes in two git refs and returns a list of PR numbers of all PRs merged between those two refs
*
* @param {String} fromRef
* @param {String} toRef
* @returns {Array<String>} – Pull request numbers
*/
function getPullRequestsMergedBetween(fromRef, toRef) {
const targetMergeList = getMergeLogsAsJSON(fromRef, toRef);
console.log(`Commits made between ${fromRef} and ${toRef}:`, targetMergeList);

// Get the full history on this branch, inclusive of the oldest commit from our target comparison
const oldestCommit = _.last(targetMergeList).commit;
const fullMergeList = getMergeLogsAsJSON(oldestCommit, 'HEAD');

// Remove from the final merge list any commits whose message appears in the full merge list more than once.
// This indicates that the PR should not be included in our list because it is a duplicate, and thus has already been processed by our CI
// See https://github.com/Expensify/App/issues/4977 for details
const duplicateMergeList = _.chain(fullMergeList)
.groupBy('subject')
.values()
.filter(i => i.length > 1)
.flatten()
.pluck('commit')
.value();
const finalMergeList = _.filter(targetMergeList, i => !_.contains(duplicateMergeList, i.commit));
console.log('Filtered out the following commits which were duplicated in the full git log:', _.difference(targetMergeList, finalMergeList));

// Find which commit messages correspond to merged PR's
const pullRequestNumbers = getValidMergedPRs(_.pluck(finalMergeList, 'subject'));
console.log(`List of pull requests merged between ${fromRef} and ${toRef}`, pullRequestNumbers);
return pullRequestNumbers;
}

module.exports = {
getValidMergedPRs,
getPullRequestsMergedBetween,
};

Expand Down
91 changes: 66 additions & 25 deletions .github/libs/GitUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,83 @@ const _ = require('underscore');
const {execSync} = require('child_process');

/**
* Takes in two git refs and returns a list of PR numbers of all PRs merged between those two refs
* Get merge logs between two refs (inclusive) as a JavaScript object.
*
* @param {String} fromRef
* @param {String} toRef
* @returns {Array}
* @returns {Object<{commit: String, subject: String}>}
*/
function getPullRequestsMergedBetween(fromRef, toRef) {
const command = `git log --format="{[%B]}" ${fromRef}...${toRef}`;
function getMergeLogsAsJSON(fromRef, toRef) {
const command = `git log --format='{"commit": "%H", "subject": "%s"},' ${fromRef}^...${toRef}`;
console.log('Getting pull requests merged between the following refs:', fromRef, toRef);
console.log('Running command: ', command);
const localGitLogs = execSync(command).toString();

// Parse the git log into an array of commit messages between the two refs
const commitMessages = _.map(
[...localGitLogs.matchAll(/{\[([\s\S]*?)\]}/gm)],
match => match[1],
);
console.log(`A list of commits made between ${fromRef} and ${toRef}:\n${commitMessages}`);

// We need to find which commit messages correspond to merge commits and get PR numbers.
// Additionally, we omit merge commits made while cherry picking using negative lookahead in the regexp.
const pullRequestIDs = _.reduce(commitMessages, (mergedPRs, commitMessage) => {
const mergeCommits = [
...commitMessage.matchAll(/Merge pull request #(\d{1,6}) from (?!(?:Expensify\/(?:master|main|version-))|(?:([\s\S]*?)\(cherry picked from commit .*\)\s*))/gm),
];

// Get the PR number of the first match (there should not be multiple matches in one commit message)
if (_.size(mergeCommits)) {
mergedPRs.push(mergeCommits[0][1]);
const result = execSync(command).toString().trim();

// Remove any double-quotes from commit subjects
const sanitizedOutput = result
.replace(/(?<="subject": ").*(?="})/g, subject => subject.replace(/"/g, "'"));

// Then format as JSON and convert to a proper JS object
const json = `[${sanitizedOutput}]`.replace('},]', '}]');
return JSON.parse(json);
}

/**
* Parse merged PRs, excluding those from irrelevant branches.
*
* @param {Array<String>} commitMessages
* @returns {Array<String>}
*/
function getValidMergedPRs(commitMessages) {
return _.reduce(commitMessages, (mergedPRs, commitMessage) => {
if (!_.isString(commitMessage)) {
return mergedPRs;
}

const match = commitMessage.match(/Merge pull request #(\d+) from (?!Expensify\/(?:master|main|version-|update-staging-from-main|update-production-from-staging))/);
if (!_.isNull(match) && match[1]) {
mergedPRs.push(match[1]);
}

return mergedPRs;
}, []);
console.log(`A list of pull requests merged between ${fromRef} and ${toRef}:\n${pullRequestIDs}`);
return pullRequestIDs;
}

/**
* Takes in two git refs and returns a list of PR numbers of all PRs merged between those two refs
*
* @param {String} fromRef
* @param {String} toRef
* @returns {Array<String>} – Pull request numbers
*/
function getPullRequestsMergedBetween(fromRef, toRef) {
const targetMergeList = getMergeLogsAsJSON(fromRef, toRef);
console.log(`Commits made between ${fromRef} and ${toRef}:`, targetMergeList);

// Get the full history on this branch, inclusive of the oldest commit from our target comparison
const oldestCommit = _.last(targetMergeList).commit;
const fullMergeList = getMergeLogsAsJSON(oldestCommit, 'HEAD');

// Remove from the final merge list any commits whose message appears in the full merge list more than once.
// This indicates that the PR should not be included in our list because it is a duplicate, and thus has already been processed by our CI
// See https://github.com/Expensify/App/issues/4977 for details
const duplicateMergeList = _.chain(fullMergeList)
.groupBy('subject')
.values()
.filter(i => i.length > 1)
.flatten()
.pluck('commit')
.value();
const finalMergeList = _.filter(targetMergeList, i => !_.contains(duplicateMergeList, i.commit));
console.log('Filtered out the following commits which were duplicated in the full git log:', _.difference(targetMergeList, finalMergeList));

// Find which commit messages correspond to merged PR's
const pullRequestNumbers = getValidMergedPRs(_.pluck(finalMergeList, 'subject'));
console.log(`List of pull requests merged between ${fromRef} and ${toRef}`, pullRequestNumbers);
return pullRequestNumbers;
}

module.exports = {
getValidMergedPRs,
getPullRequestsMergedBetween,
};
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,20 @@ jobs:
- run: npm run test
env:
CI: true

- name: Decrypt OSBotify GPG key
run: cd .github/workflows && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output OSBotify-private-key.asc OSBotify-private-key.asc.gpg
env:
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}

- name: Import OSBotify GPG Key
run: cd .github/workflows && gpg --import OSBotify-private-key.asc

- name: Set up git for OSBotify
run: |
git config --global user.name OSBotify
git config --global user.email infra+osbotify@expensify.com
git config --global user.signingkey 367811D53E34168C
git config --global commit.gpgsign true

- run: tests/unit/getPullRequestsMergedBetweenTest.sh
6 changes: 3 additions & 3 deletions .github/workflows/updateProtectedBranch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ jobs:
- name: Set New Version
run: echo "NEW_VERSION=$(npm run print-version --silent)" >> $GITHUB_ENV

- name: Decrypt Botify GPG key
- name: Decrypt OSBotify GPG key
run: cd .github/workflows && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output OSBotify-private-key.asc OSBotify-private-key.asc.gpg
env:
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}

- name: Import Botify GPG Key
- name: Import OSBotify GPG Key
run: cd .github/workflows && gpg --import OSBotify-private-key.asc

- name: Set up git for Botify
- name: Set up git for OSBotify
run: |
git config user.signingkey 367811D53E34168C
git config commit.gpgsign true
Expand Down
32 changes: 32 additions & 0 deletions shellUtils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash

GREEN=$'\e[1;32m'
RED=$'\e[1;31m'
BLUE=$'\e[1;34m'
TITLE=$'\e[1;4;34m'
RESET=$'\e[0m'

function success {
echo "🎉 $GREEN$1$RESET"
}

function error {
echo "💥 $RED$1$RESET"
}

function info {
echo "$BLUE$1$RESET"
}

function title {
printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET"
}

function assert_equal {
if [[ "$1" != "$2" ]]; then
error "Assertion failed: $1 is not equal to $2"
exit 1
else
success "Assertion passed: $1 is equal to $1"
fi
}
Loading