generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 4
fix(INFRA-3081): implement duplicate detection for all commit types #254
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
3d5da86
fix(INFRA-3081): implement duplicate detection for all commit types
Qbandev a68cb43
fix: use exact match for duplicate detection of direct commits
Qbandev b747041
chore: update src/get-new-changes.test.ts
Qbandev 6b446cc
fix: normalize whitespace in duplicate detection for direct commits
Qbandev 0e1438d
perf: pre-normalize descriptions to avoid redundant trim operations
Qbandev cac8ef8
fix: skip feature branch commits when merge commit exists
Qbandev 4e7a9cb
perf: use Set for O(1) merge commit lookup instead of O(N*M) iteration
Qbandev 124879c
debug: add logging to trace commit extraction and filtering
Qbandev 1a349c4
chore: update debug logs
Qbandev e7bf9cc
fix: update excluded keywords for commit filtering
Qbandev 60dc116
refactor: remove debug logging and streamline commit filtering
Qbandev 619977e
fix: ensure GITHUB_TOKEN is set for Octokit initialization
Qbandev d4fea63
fix: improve subject categorization for merge commits
Qbandev 2cc9952
refactor: enhance commit deduplication logic and structure
Qbandev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,339 @@ | ||
| import { getNewChangeEntries } from './get-new-changes'; | ||
| import { runCommand, runCommandAndSplit } from './run-command'; | ||
|
|
||
| jest.mock('./run-command'); | ||
|
|
||
| const mockRunCommand = runCommand as jest.MockedFunction<typeof runCommand>; | ||
| const mockRunCommandAndSplit = runCommandAndSplit as jest.MockedFunction< | ||
| typeof runCommandAndSplit | ||
| >; | ||
|
|
||
| const repoUrl = 'https://github.com/MetaMask/metamask-mobile'; | ||
|
|
||
| describe('getNewChangeEntries', () => { | ||
| beforeEach(() => { | ||
| mockRunCommandAndSplit.mockResolvedValue([]); | ||
| }); | ||
|
|
||
| describe('PR-tagged commits', () => { | ||
| it('should include commits with PR numbers', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1', 'commit2']); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: It's hard to tell what these mocks are intended to do, because both We could improve this by using a mock implementation that validates expected params maybe? |
||
| mockRunCommand | ||
| .mockResolvedValueOnce('add feature (#12345)') | ||
| .mockResolvedValueOnce('bug fix (#12346)'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: | ||
| 'add feature ([#12345](https://github.com/MetaMask/metamask-mobile/pull/12345))', | ||
| subject: 'add feature (#12345)', | ||
| }, | ||
| { | ||
| description: | ||
| 'bug fix ([#12346](https://github.com/MetaMask/metamask-mobile/pull/12346))', | ||
| subject: 'bug fix (#12346)', | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it('should exclude commits with PR numbers already in changelog', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1', 'commit2']); | ||
| mockRunCommand | ||
| .mockResolvedValueOnce('add feature (#12345)') | ||
| .mockResolvedValueOnce('bug fix (#12346)'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: ['12345'], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: | ||
| 'bug fix ([#12346](https://github.com/MetaMask/metamask-mobile/pull/12346))', | ||
| subject: 'bug fix (#12346)', | ||
| }, | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('direct commits (no PR numbers)', () => { | ||
| it('should include direct commits', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1', 'commit2']); | ||
| mockRunCommand | ||
| .mockResolvedValueOnce('Update Attributions') | ||
| .mockResolvedValueOnce('Bump version to 7.58.0'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: 'Update Attributions', | ||
| subject: 'Update Attributions', | ||
| }, | ||
| { | ||
| description: 'Bump version to 7.58.0', | ||
| subject: 'Bump version to 7.58.0', | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it('should exclude direct commits already in changelog', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1', 'commit2']); | ||
| mockRunCommand | ||
| .mockResolvedValueOnce('Update Attributions') | ||
| .mockResolvedValueOnce('Bump version to 7.58.0'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: ['Update Attributions'], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: 'Bump version to 7.58.0', | ||
| subject: 'Bump version to 7.58.0', | ||
| }, | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('merge commits', () => { | ||
| it('should extract PR numbers from merge commits', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1', 'commit2']); | ||
| mockRunCommand | ||
| .mockResolvedValueOnce('Merge pull request #12345 from feature-branch') | ||
| .mockResolvedValueOnce('Merge pull request #12346 from fix-branch'); | ||
| // Mock body fetches for merge commits | ||
| mockRunCommandAndSplit | ||
| .mockResolvedValueOnce(['implement new feature']) | ||
| .mockResolvedValueOnce(['fix critical bug']); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: | ||
| 'implement new feature ([#12345](https://github.com/MetaMask/metamask-mobile/pull/12345))', | ||
| subject: 'implement new feature', | ||
| }, | ||
| { | ||
| description: | ||
| 'fix critical bug ([#12346](https://github.com/MetaMask/metamask-mobile/pull/12346))', | ||
| subject: 'fix critical bug', | ||
| }, | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('squash merge deduplication', () => { | ||
| it('should skip direct commit when PR-tagged commit with same description exists', async () => { | ||
| // Simulates squash merge where both original and merged commits appear | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1', 'commit2']); | ||
| mockRunCommand | ||
| .mockResolvedValueOnce('add new feature') // Direct commit (no PR) | ||
| .mockResolvedValueOnce('add new feature (#12345)'); // PR-tagged commit with same description | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| // Should only include the PR-tagged version | ||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: | ||
| 'add new feature ([#12345](https://github.com/MetaMask/metamask-mobile/pull/12345))', | ||
| subject: 'add new feature (#12345)', | ||
| }, | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('duplicate detection', () => { | ||
| it('should return empty array when all commits are duplicates', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce([ | ||
| 'commit1', | ||
| 'commit2', | ||
| 'commit3', | ||
| ]); | ||
| mockRunCommand | ||
| .mockResolvedValueOnce('add feature (#12345)') | ||
| .mockResolvedValueOnce('Update Attributions') | ||
| .mockResolvedValueOnce('Bump version'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: ['12345'], | ||
| loggedDescriptions: ['Update Attributions', 'Bump version'], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('edge cases', () => { | ||
| it('should return empty array when there are no commits', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce([]); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([]); | ||
| }); | ||
|
|
||
| it('should use HEAD as commit range when no tag is available', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1']); | ||
| mockRunCommand.mockResolvedValueOnce('add feature (#12345)'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: null, | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(mockRunCommandAndSplit).toHaveBeenCalledWith('git', [ | ||
| 'rev-list', | ||
| 'HEAD', | ||
| ]); | ||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: | ||
| 'add feature ([#12345](https://github.com/MetaMask/metamask-mobile/pull/12345))', | ||
| subject: 'add feature (#12345)', | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it('should throw error when git show returns empty subject', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1']); | ||
| mockRunCommand.mockResolvedValueOnce(''); | ||
|
|
||
| await expect( | ||
| getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }), | ||
| ).rejects.toThrow( | ||
| '"git show" returned empty subject for commit "commit1".', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('PR link formatting', () => { | ||
| it('should include full PR link when useShortPrLink is false', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1']); | ||
| mockRunCommand.mockResolvedValueOnce('add feature (#12345)'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: | ||
| 'add feature ([#12345](https://github.com/MetaMask/metamask-mobile/pull/12345))', | ||
| subject: 'add feature (#12345)', | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it('should use short PR link when useShortPrLink is true', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1']); | ||
| mockRunCommand.mockResolvedValueOnce('add feature (#12345)'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: true, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: 'add feature (#12345)', | ||
| subject: 'add feature (#12345)', | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it('should not add PR link suffix for direct commits', async () => { | ||
| mockRunCommandAndSplit.mockResolvedValueOnce(['commit1']); | ||
| mockRunCommand.mockResolvedValueOnce('Update Attributions'); | ||
|
|
||
| const result = await getNewChangeEntries({ | ||
| mostRecentTag: 'v1.0.0', | ||
| repoUrl, | ||
| loggedPrNumbers: [], | ||
| loggedDescriptions: [], | ||
| useChangelogEntry: false, | ||
| useShortPrLink: false, | ||
| }); | ||
|
|
||
| expect(result).toStrictEqual([ | ||
| { | ||
| description: 'Update Attributions', | ||
| subject: 'Update Attributions', | ||
| }, | ||
| ]); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this change belong in this PR? It seems separate from the problem this PR is solving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is related to the PR's goal because the original keyword
'merge'was too broad and would exclude any commit containing "merge" in the description (e.g., "feat: merge user profiles into dashboard"). Changing to'Merge pull request'is more specific and only excludes actual GitHub merge commit messages.That said, I can remove it if you prefer to keep the scope narrower. Let me know your preference.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to make this change separately I think. There are other entries that look too broad as well. e.g.
cp-would exclude any urgent changes that we cherry-pick (which certainly we want documented for the hotfix release).The whole strategy is a bit questionable to be honest 🤔 It's fairly common for teams to merge functional changes under PRs with a title related to non-functional changes like e2e tests. Certainly not a good practice, but we don't do enough to detect or prevent it. But I guess that's out of scope for this PR, it's already done.