Skip to content

Commit 02f7799

Browse files
committed
TT-16103: trigger release to suggested branch after merge
1 parent c0d4c3d commit 02f7799

8 files changed

Lines changed: 571 additions & 151 deletions

File tree

.github/workflows/branch-suggestion.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Branch Suggestion for PRs
22

33
on:
44
pull_request:
5-
types: [opened, synchronize, reopened, ready_for_review]
5+
types: [opened, synchronize, reopened, ready_for_review, closed]
66
workflow_call:
77
secrets:
88
JIRA_READ_AUTH:
@@ -53,6 +53,8 @@ jobs:
5353
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5454
PR_TITLE: ${{ github.event.pull_request.title }}
5555
PR_NUMBER: ${{ github.event.pull_request.number }}
56+
PR_MERGED: ${{ github.event.pull_request.merged }}
57+
PR_ACTION: ${{ github.event.action }}
5658
REPOSITORY: ${{ github.repository }}
5759
BRANCH_NAME: ${{ github.head_ref }}
5860
run: |

.github/workflows/example-usage.yml.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name: PR Branch Suggestions
55

66
on:
77
pull_request:
8-
types: [opened, synchronize, reopened]
8+
types: [opened, synchronize, reopened, ready_for_review, closed]
99

1010
permissions:
1111
pull-requests: write

branch-suggestion/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Add this workflow to your repository to enable automatic branch suggestions:
2828
```yaml
2929
on:
3030
pull_request:
31-
types: [opened, synchronize, reopened]
31+
types: [opened, synchronize, reopened, ready_for_review, closed]
3232

3333
permissions:
3434
pull-requests: write

branch-suggestion/branch_suggestion.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ checks:
2020
exec: |
2121
set -e
2222
23+
# Early exit if PR is closed but not merged (avoid unnecessary API calls)
24+
if [ "$PR_ACTION" = "closed" ] && [ "$PR_MERGED" != "true" ]; then
25+
echo "ℹ️ PR closed without merge. Skipping analysis."
26+
exit 0
27+
fi
28+
2329
# Read from environment variables with defaults for local testing
2430
PR_TITLE="${PR_TITLE:-TT-12345: Test PR}"
2531
BRANCH_NAME="${BRANCH_NAME:-feature/TT-12345-test}"
@@ -84,6 +90,9 @@ checks:
8490
# Extract markdown and save for PR comment (use printf to avoid escape sequence issues)
8591
printf '%s\n' "$MATCH_RESULT" | jq -r '.markdown' > /tmp/branch_suggestion_markdown.txt
8692
93+
# Save full JSON result for other steps
94+
printf '%s\n' "$MATCH_RESULT" > /tmp/branch_suggestion_results.json
95+
8796
# Output full result to stdout
8897
printf '%s\n' "$MATCH_RESULT"
8998
@@ -115,3 +124,38 @@ checks:
115124
116125
# Clean up
117126
rm -f /tmp/branch_suggestion_markdown.txt
127+
128+
# ============================================================================
129+
# STEP 3: POST RELEASE COMMANDS ON MERGE
130+
# ============================================================================
131+
# Automatically posts /release comments when a PR is merged
132+
# ============================================================================
133+
post-release-commands:
134+
type: command
135+
depends_on: [analyze-and-suggest]
136+
tags: ["remote"]
137+
timeout: 30000
138+
exec: |
139+
set -e
140+
141+
# Only run if the PR was merged
142+
if [ "$PR_MERGED" != "true" ]; then
143+
echo "ℹ️ PR not merged. Skipping release commands."
144+
rm -f /tmp/branch_suggestion_results.json
145+
exit 0
146+
fi
147+
148+
# Read from environment variables with defaults for local testing
149+
REPO="${REPOSITORY:-TykTechnologies/tyk}"
150+
PR_NUMBER="${PR_NUMBER:-123}"
151+
152+
if [ ! -f /tmp/branch_suggestion_results.json ]; then
153+
echo "ℹ️ Results file not found. Skipping release commands." >&2
154+
exit 0
155+
fi
156+
157+
# Post release comments
158+
node scripts/github/post-release-commands.js "$REPO" "$PR_NUMBER" "$(cat /tmp/branch_suggestion_results.json)"
159+
160+
# Clean up
161+
rm -f /tmp/branch_suggestion_results.json
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { postReleaseComments } from '../post-release-commands.js';
3+
import { createPRComment, findCommentByMarker, parseRepo } from '../github-api.js';
4+
5+
vi.mock('../github-api.js', () => ({
6+
createPRComment: vi.fn(),
7+
findCommentByMarker: vi.fn(),
8+
parseRepo: vi.fn()
9+
}));
10+
11+
describe('postReleaseComments', () => {
12+
const repository = 'TykTechnologies/tyk';
13+
const prNumber = 123;
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
parseRepo.mockReturnValue({ owner: 'TykTechnologies', repo: 'tyk' });
18+
// Mock console methods to avoid noise in tests
19+
vi.spyOn(console, 'log').mockImplementation(() => {});
20+
vi.spyOn(console, 'error').mockImplementation(() => {});
21+
});
22+
23+
afterEach(() => {
24+
vi.restoreAllMocks();
25+
});
26+
27+
it('should skip if no match results are found', async () => {
28+
await postReleaseComments(repository, prNumber, {});
29+
expect(createPRComment).not.toHaveBeenCalled();
30+
expect(findCommentByMarker).not.toHaveBeenCalled();
31+
});
32+
33+
it('should skip if no eligible release branches are found', async () => {
34+
const matchData = {
35+
matchResults: [
36+
{
37+
fixVersion: '5.8.1',
38+
branches: [
39+
{ branch: 'master', priority: 'required' },
40+
{ branch: 'optional-branch', priority: 'optional' }
41+
]
42+
}
43+
]
44+
};
45+
await postReleaseComments(repository, prNumber, matchData);
46+
expect(createPRComment).not.toHaveBeenCalled();
47+
});
48+
49+
it('should post release comments for required and recommended branches', async () => {
50+
const matchData = {
51+
matchResults: [
52+
{
53+
fixVersion: '5.8.1',
54+
branches: [
55+
{ branch: 'release-5.8.1', priority: 'required' },
56+
{ branch: 'release-5.8', priority: 'recommended' },
57+
{ branch: 'master', priority: 'required' }
58+
]
59+
}
60+
]
61+
};
62+
63+
findCommentByMarker.mockResolvedValue(null); // No existing comments
64+
createPRComment.mockResolvedValue({ id: 1 });
65+
66+
await postReleaseComments(repository, prNumber, matchData);
67+
68+
expect(findCommentByMarker).toHaveBeenCalledTimes(2);
69+
expect(createPRComment).toHaveBeenCalledTimes(2);
70+
expect(createPRComment).toHaveBeenCalledWith(
71+
'TykTechnologies',
72+
'tyk',
73+
123,
74+
expect.stringContaining('/release to release-5.8.1')
75+
);
76+
expect(createPRComment).toHaveBeenCalledWith(
77+
'TykTechnologies',
78+
'tyk',
79+
123,
80+
expect.stringContaining('/release to release-5.8')
81+
);
82+
});
83+
84+
it('should skip branches that already have a release comment', async () => {
85+
const matchData = {
86+
matchResults: [
87+
{
88+
fixVersion: '5.8.1',
89+
branches: [
90+
{ branch: 'release-5.8.1', priority: 'required' },
91+
{ branch: 'release-5.8', priority: 'recommended' }
92+
]
93+
}
94+
]
95+
};
96+
97+
// Mock that release-5.8.1 already has a comment
98+
findCommentByMarker.mockImplementation((owner, repo, pr, marker) => {
99+
if (marker.includes('release-5.8.1')) return { id: 1 };
100+
return null;
101+
});
102+
103+
await postReleaseComments(repository, prNumber, matchData);
104+
105+
expect(findCommentByMarker).toHaveBeenCalledTimes(2);
106+
expect(createPRComment).toHaveBeenCalledTimes(1);
107+
expect(createPRComment).toHaveBeenCalledWith(
108+
'TykTechnologies',
109+
'tyk',
110+
123,
111+
expect.stringContaining('/release to release-5.8')
112+
);
113+
});
114+
115+
it('should handle API errors for one branch and continue with others', async () => {
116+
const matchData = {
117+
matchResults: [
118+
{
119+
fixVersion: '5.8.1',
120+
branches: [
121+
{ branch: 'release-5.8.1', priority: 'required' },
122+
{ branch: 'release-5.8', priority: 'recommended' }
123+
]
124+
}
125+
]
126+
};
127+
128+
findCommentByMarker.mockResolvedValue(null);
129+
createPRComment.mockImplementation((owner, repo, pr, body) => {
130+
if (body.includes('release-5.8.1')) throw new Error('API Error');
131+
return { id: 2 };
132+
});
133+
134+
await expect(postReleaseComments(repository, prNumber, matchData)).rejects.toThrow(
135+
'Failed to post release comments for required branches'
136+
);
137+
138+
expect(createPRComment).toHaveBeenCalledTimes(2);
139+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to process release comment for release-5.8.1'));
140+
});
141+
142+
it('should not throw if only recommended branch fails', async () => {
143+
const matchData = {
144+
matchResults: [
145+
{
146+
fixVersion: '5.8.1',
147+
branches: [
148+
{ branch: 'release-5.8', priority: 'recommended' }
149+
]
150+
}
151+
]
152+
};
153+
154+
findCommentByMarker.mockResolvedValue(null);
155+
createPRComment.mockRejectedValue(new Error('API Error'));
156+
157+
await postReleaseComments(repository, prNumber, matchData);
158+
159+
expect(createPRComment).toHaveBeenCalledTimes(1);
160+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to process release comment for release-5.8'));
161+
});
162+
163+
it('should not throw if a match result is missing branches array', async () => {
164+
const matchData = {
165+
matchResults: [
166+
{
167+
fixVersion: '5.8.1'
168+
}
169+
]
170+
};
171+
172+
await postReleaseComments(repository, prNumber, matchData);
173+
expect(createPRComment).not.toHaveBeenCalled();
174+
});
175+
176+
it('should collect unique branches across multiple fix versions', async () => {
177+
const matchData = {
178+
matchResults: [
179+
{
180+
fixVersion: '5.8.1',
181+
branches: [{ branch: 'release-5.8', priority: 'required' }]
182+
},
183+
{
184+
fixVersion: '5.8.0',
185+
branches: [{ branch: 'release-5.8', priority: 'required' }]
186+
}
187+
]
188+
};
189+
190+
findCommentByMarker.mockResolvedValue(null);
191+
192+
await postReleaseComments(repository, prNumber, matchData);
193+
194+
expect(findCommentByMarker).toHaveBeenCalledTimes(1);
195+
expect(createPRComment).toHaveBeenCalledTimes(1);
196+
});
197+
});

0 commit comments

Comments
 (0)