Skip to content

Commit 1aa5a73

Browse files
ady-beraudady-test
authored andcommitted
Create congratulations bot (#5404)
- Created congratulations bot : <img width="939" alt="Screenshot 2024-05-14 at 12 47 13" src="https://github.com/twentyhq/twenty/assets/102751374/5138515f-fe4d-4c6d-9c7a-0240accbfca9"> - Modified OG image - Added png extension to OG image route To be noted: The bot will not work until the new API route is not deployed. Please check OG image with Cloudflare cache. --------- Co-authored-by: Ady Beraud <[email protected]>
1 parent 0e04a68 commit 1aa5a73

File tree

17 files changed

+277
-73
lines changed

17 files changed

+277
-73
lines changed

.github/workflows/ci-utils.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
# see: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
66
# and: https://github.com/facebook/react-native/pull/34370/files
77
pull_request_target:
8+
types: [opened, synchronize, reopened, closed]
89
permissions:
910
actions: write
1011
checks: write
@@ -19,6 +20,7 @@ concurrency:
1920
jobs:
2021
danger-js:
2122
runs-on: ubuntu-latest
23+
if: github.event.action != 'closed'
2224
steps:
2325
- uses: actions/checkout@v4
2426
- name: Install dependencies
@@ -27,3 +29,15 @@ jobs:
2729
run: cd packages/twenty-utils && npx nx danger:ci
2830
env:
2931
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
32+
33+
congratulate:
34+
runs-on: ubuntu-latest
35+
if: github.event.action == 'closed' && github.event.pull_request.merged == true
36+
steps:
37+
- uses: actions/checkout@v4
38+
- name: Install dependencies
39+
uses: ./.github/workflows/actions/yarn-install
40+
- name: Run congratulate-dangerfile.js
41+
run: cd packages/twenty-utils && npx nx danger:congratulate
42+
env:
43+
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { danger } from 'danger';
2+
3+
const ordinalSuffix = (number) => {
4+
const v = number % 100;
5+
if (v === 11 || v === 12 || v === 13) {
6+
return number + 'th';
7+
}
8+
const suffixes = { 1: 'st', 2: 'nd', 3: 'rd' };
9+
return number + (suffixes[v % 10] || 'th');
10+
};
11+
12+
const fetchContributorStats = async (username: string) => {
13+
const apiUrl = `https://twenty.com/api/contributors/contributorStats/${username}`;
14+
15+
const response = await fetch(apiUrl);
16+
const data = await response.json();
17+
return data;
18+
};
19+
20+
const fetchContributorImage = async (username: string) => {
21+
const apiUrl = `https://twenty.com/api/contributors/${username}/og.png`;
22+
23+
await fetch(apiUrl);
24+
};
25+
26+
const getTeamMembers = async () => {
27+
const org = 'twentyhq';
28+
const team_slug = 'core-team';
29+
const response = await danger.github.api.teams.listMembersInOrg({
30+
org,
31+
team_slug,
32+
});
33+
return response.data.map((user) => user.login);
34+
};
35+
36+
const runCongratulate = async () => {
37+
const pullRequest = danger.github.pr;
38+
const userName = pullRequest.user.login;
39+
40+
const staticExcludedUsers = [
41+
'dependabot',
42+
'cyborch',
43+
'emilienchvt',
44+
'Samox',
45+
'charlesBochet',
46+
'gitstart-app',
47+
'thaisguigon',
48+
'lucasbordeau',
49+
'magrinj',
50+
'Weiko',
51+
'gitstart-twenty',
52+
'bosiraphael',
53+
'martmull',
54+
'FelixMalfait',
55+
'thomtrp',
56+
'Bonapara',
57+
'nimraahmed',
58+
'ady-beraud',
59+
];
60+
61+
const teamMembers = await getTeamMembers();
62+
63+
const excludedUsers = new Set([...staticExcludedUsers, ...teamMembers]);
64+
65+
if (excludedUsers.has(userName)) {
66+
return;
67+
}
68+
69+
const { data: pullRequests } =
70+
await danger.github.api.rest.search.issuesAndPullRequests({
71+
q: `is:pr author:${userName} is:closed repo:twentyhq/twenty`,
72+
per_page: 2,
73+
page: 1,
74+
});
75+
76+
const isFirstPR = pullRequests.total_count === 1;
77+
78+
if (isFirstPR) {
79+
return;
80+
}
81+
82+
const stats = await fetchContributorStats(userName);
83+
const contributorUrl = `https://twenty.com/contributors/${userName}`;
84+
85+
// Pre-fetch to trigger cloudflare cache
86+
await fetchContributorImage(userName);
87+
88+
const message =
89+
`Thanks @${userName} for your contribution!\n` +
90+
`This marks your **${ordinalSuffix(
91+
stats.mergedPRsCount,
92+
)}** PR on the repo. ` +
93+
`You're **top ${stats.rank}%** of all our contributors 🎉\n` +
94+
`[See contributor page](${contributorUrl}) - ` +
95+
`[Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=${contributorUrl}) - ` +
96+
`[Share on Twitter](https://www.twitter.com/share?url=${contributorUrl})\n\n` +
97+
`![Contributions](https://twenty.com/api/contributors/${userName}/og.png)`;
98+
99+
await danger.github.api.rest.issues.createComment({
100+
owner: danger.github.thisPR.owner,
101+
repo: danger.github.thisPR.repo,
102+
issue_number: danger.github.thisPR.pull_number,
103+
body: message,
104+
});
105+
};
106+
107+
if (danger.github && danger.github.pr.merged) {
108+
runCongratulate();
109+
}

packages/twenty-utils/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"scripts": {
55
"nx": "NX_DEFAULT_PROJECT=twenty-front node ../../node_modules/nx/bin/nx.js",
66
"danger:ci": "danger ci --use-github-checks --failOnErrors",
7+
"danger:congratulate": "danger ci --dangerfile ./congratulate-dangerfile.ts --use-github-checks --failOnErrors",
78
"release": "node release.js"
89
}
910
}

packages/twenty-website/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"build": "npx next build",
99
"start": "npx next start",
1010
"lint": "npx next lint",
11-
"github:sync": "npx tsx src/github/github-sync.ts --pageLimit 1",
12-
"github:init": "npx tsx src/github/github-sync.ts",
11+
"github:sync": "npx tsx src/github/github-sync.ts",
12+
"github:init": "npx tsx src/github/github-sync.ts --isFullSync",
1313
"database:migrate": "npx tsx src/database/migrate-database.ts",
1414
"database:generate:pg": "npx drizzle-kit generate:pg --config=src/database/drizzle-posgres.config.ts"
1515
},

packages/twenty-website/src/app/_components/contributors/ProfileSharing.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ interface ProfileProps {
5555

5656
export const ProfileSharing = ({ username }: ProfileProps) => {
5757
const [loading, setLoading] = useState(false);
58-
const baseUrl = `${window.location.protocol}//${window.location.host}`;
58+
const baseUrl = 'https://twenty.com';
5959
const contributorUrl = `${baseUrl}/contributors/${username}`;
6060

6161
const handleDownload = async () => {
@@ -101,7 +101,7 @@ export const ProfileSharing = ({ username }: ProfileProps) => {
101101
)}
102102
</StyledButton>
103103
<StyledButton
104-
href={`http://www.twitter.com/share?url=${contributorUrl}`}
104+
href={`https://www.twitter.com/share?url=${contributorUrl}`}
105105
target="blank"
106106
>
107107
<XIcon color="black" size="24px" /> Share on X

packages/twenty-website/src/app/api/contributors/og-image/[slug]/route.tsx renamed to packages/twenty-website/src/app/api/contributors/[slug]/og.png/route.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { format } from 'date-fns';
22
import { ImageResponse } from 'next/og';
33

44
import {
5-
bottomBackgroundImage,
5+
backgroundImage,
66
container,
77
contributorInfo,
88
contributorInfoBox,
@@ -15,8 +15,7 @@ import {
1515
profileInfoContainer,
1616
profileUsernameHeader,
1717
styledContributorAvatar,
18-
topBackgroundImage,
19-
} from '@/app/api/contributors/og-image/[slug]/style';
18+
} from '@/app/api/contributors/[slug]/og.png/style';
2019
import { getContributorActivity } from '@/app/contributors/utils/get-contributor-activity';
2120

2221
const GABARITO_FONT_CDN_URL =
@@ -33,8 +32,10 @@ const getGabarito = async () => {
3332
export async function GET(request: Request) {
3433
try {
3534
const url = request.url;
36-
37-
const username = url.split('/')?.pop() || '';
35+
const splitUrl = url.split('/');
36+
const usernameIndex =
37+
splitUrl.findIndex((part) => part === 'contributors') + 1;
38+
const username = splitUrl[usernameIndex];
3839

3940
const contributorActivity = await getContributorActivity(username);
4041
if (contributorActivity) {
@@ -45,11 +46,11 @@ export async function GET(request: Request) {
4546
activeDays,
4647
contributorAvatar,
4748
} = contributorActivity;
48-
return await new ImageResponse(
49+
50+
const imageResponse = await new ImageResponse(
4951
(
5052
<div style={container}>
51-
<div style={topBackgroundImage}></div>
52-
<div style={bottomBackgroundImage}></div>
53+
<div style={backgroundImage}></div>
5354
<div style={profileContainer}>
5455
<img src={contributorAvatar} style={styledContributorAvatar} />
5556
<div style={profileInfoContainer}>
@@ -59,8 +60,8 @@ export async function GET(request: Request) {
5960
</h2>
6061
</div>
6162
<svg
62-
width="96"
63-
height="96"
63+
width="134"
64+
height="134"
6465
viewBox="0 0 136 136"
6566
fill="none"
6667
xmlns="http://www.w3.org/2000/svg"
@@ -122,6 +123,7 @@ export async function GET(request: Request) {
122123
],
123124
},
124125
);
126+
return imageResponse;
125127
}
126128
} catch (error) {
127129
return new Response(`error: ${error}`, {

packages/twenty-website/src/app/api/contributors/og-image/[slug]/style.ts renamed to packages/twenty-website/src/app/api/contributors/[slug]/og.png/style.ts

+26-32
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,36 @@ export const container: CSSProperties = {
1414
fontFamily: 'Gabarito',
1515
};
1616

17-
export const topBackgroundImage: CSSProperties = {
18-
backgroundImage: `url(${BACKGROUND_IMAGE_URL})`,
17+
export const backgroundImage: CSSProperties = {
1918
position: 'absolute',
20-
zIndex: '-1',
21-
width: '1300px',
22-
height: '250px',
23-
transform: 'rotate(-11deg)',
24-
opacity: '0.2',
25-
top: '-100',
26-
left: '-25',
27-
};
28-
29-
export const bottomBackgroundImage: CSSProperties = {
30-
backgroundImage: `url(${BACKGROUND_IMAGE_URL})`,
31-
position: 'absolute',
32-
zIndex: '-1',
33-
width: '1300px',
34-
height: '250px',
35-
transform: 'rotate(-11deg)',
36-
opacity: '0.2',
37-
bottom: '-120',
38-
right: '-40',
19+
width: '1250px',
20+
height: '850px',
21+
transform: 'rotate(-7deg)',
22+
opacity: '0.8',
23+
backgroundImage: `
24+
linear-gradient(
25+
158.4deg,
26+
rgba(255, 255, 255, 0.8) 30.69%,
27+
#FFFFFF 35.12%,
28+
rgba(255, 255, 255, 0.8) 60.27%,
29+
rgba(255, 255, 255, 0.64) 38.88%
30+
),
31+
url(${BACKGROUND_IMAGE_URL})`,
3932
};
4033

4134
export const profileContainer: CSSProperties = {
4235
display: 'flex',
4336
flexDirection: 'row',
4437
justifyContent: 'space-between',
45-
width: '780px',
46-
margin: '0px 0px 40px',
38+
width: '970px',
39+
height: '134px',
40+
margin: '0px 0px 55px',
4741
};
4842

4943
export const styledContributorAvatar = {
5044
display: 'flex',
51-
width: '96px',
52-
height: '96px',
45+
width: '134px',
46+
height: '134px',
5347
margin: '0px',
5448
border: '3px solid #141414',
5549
borderRadius: '16px',
@@ -65,7 +59,7 @@ export const profileInfoContainer: CSSProperties = {
6559

6660
export const profileUsernameHeader: CSSProperties = {
6761
margin: '0px',
68-
fontSize: '28px',
62+
fontSize: '39px',
6963
fontWeight: '700',
7064
color: '#141414',
7165
fontFamily: 'Gabarito',
@@ -74,7 +68,7 @@ export const profileUsernameHeader: CSSProperties = {
7468
export const profileContributionHeader: CSSProperties = {
7569
margin: '0px',
7670
color: '#818181',
77-
fontSize: '20px',
71+
fontSize: '27px',
7872
fontWeight: '400',
7973
};
8074

@@ -84,8 +78,8 @@ export const contributorInfoContainer: CSSProperties = {
8478
display: 'flex',
8579
flexDirection: 'row',
8680
justifyContent: 'space-around',
87-
width: '780px',
88-
height: '149px',
81+
width: '970px',
82+
height: '209px',
8983
backgroundColor: '#F1F1F1',
9084
};
9185

@@ -110,21 +104,21 @@ export const contributorInfoTitle = {
110104
color: '#B3B3B3',
111105
margin: '0px',
112106
fontWeight: '500',
113-
fontSize: '24px',
107+
fontSize: '33px',
114108
};
115109

116110
export const contributorInfoStats = {
117111
color: '#474747',
118112
margin: '0px',
119113
fontWeight: '700',
120-
fontSize: '40px',
114+
fontSize: '55px',
121115
};
122116

123117
export const infoSeparator: CSSProperties = {
124118
position: 'absolute',
125119
right: 0,
126120
display: 'flex',
127121
width: '2px',
128-
height: '85px',
122+
height: '120px',
129123
backgroundColor: '#141414',
130124
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getContributorActivity } from '@/app/contributors/utils/get-contributor-activity';
2+
import { executePartialSync } from '@/github/execute-partial-sync';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
export async function GET(request: Request) {
7+
try {
8+
const url = request.url;
9+
10+
const username = url.split('/')?.pop() || '';
11+
12+
await executePartialSync();
13+
14+
const contributorActivity = await getContributorActivity(username);
15+
16+
if (contributorActivity) {
17+
const mergedPRsCount = contributorActivity.mergedPRsCount;
18+
const rank = contributorActivity.rank;
19+
return Response.json({ mergedPRsCount, rank });
20+
}
21+
} catch (error: any) {
22+
return new Response(`Contributor stats error: ${error?.message}`, {
23+
status: 500,
24+
});
25+
}
26+
}

0 commit comments

Comments
 (0)