@hellopearl/dv-gitlab 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { createSign } from 'node:crypto';
3
+
3
4
  import { log } from '../lib/logger.mjs';
4
5
 
5
6
  const MAX_COMMITS = 30;
@@ -11,19 +12,22 @@ function env(key, fallback = '') {
11
12
  }
12
13
 
13
14
  function shellQuote(value) {
14
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
15
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
15
16
  }
16
17
 
17
18
  function git(command) {
18
19
  return execSync(`git ${command}`, {
19
20
  encoding: 'utf8',
20
- timeout: 60_000,
21
21
  stdio: ['ignore', 'pipe', 'pipe'],
22
+ timeout: 60_000,
22
23
  });
23
24
  }
24
25
 
25
26
  export function escapeSlack(text) {
26
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
27
+ return text
28
+ .replace(/&/g, '&amp;')
29
+ .replace(/</g, '&lt;')
30
+ .replace(/>/g, '&gt;');
27
31
  }
28
32
 
29
33
  export function isNoiseCommit(subject) {
@@ -31,18 +35,24 @@ export function isNoiseCommit(subject) {
31
35
  }
32
36
 
33
37
  export function filterNoiseCommits(commits) {
34
- return commits.filter((c) => !isNoiseCommit(c));
38
+ return commits.filter(c => !isNoiseCommit(c));
35
39
  }
36
40
 
37
41
  export function shouldResolveFromSourceBranch(commits) {
38
- if (!commits.length || commits.length > 3) return false;
39
- const nonMerge = commits.filter((c) => !/^merge\b/i.test(c.trim()));
40
- if (!nonMerge.length) return false;
41
- return nonMerge.every((c) => /^release:\s*/i.test(c.trim()));
42
+ if (!commits.length || commits.length > 3) {
43
+ return false;
44
+ }
45
+ const nonMerge = commits.filter(c => !/^merge\b/i.test(c.trim()));
46
+ if (!nonMerge.length) {
47
+ return false;
48
+ }
49
+ return nonMerge.every(c => /^release:\s*/i.test(c.trim()));
42
50
  }
43
51
 
44
52
  export function getCommitsInRange(from, to) {
45
- if (!from || from === '0000000000000000000000000000000000000000') return [];
53
+ if (!from || from === '0000000000000000000000000000000000000000') {
54
+ return [];
55
+ }
46
56
  try {
47
57
  const raw = git(`log ${from}..${to} --no-merges --format="%s"`);
48
58
  return raw.trim().split('\n').filter(Boolean);
@@ -53,18 +63,24 @@ export function getCommitsInRange(from, to) {
53
63
  }
54
64
 
55
65
  export function getCommitsFromSourceBranch(from, to, sourceBranch) {
56
- if (!from || from === '0000000000000000000000000000000000000000') return [];
66
+ if (!from || from === '0000000000000000000000000000000000000000') {
67
+ return [];
68
+ }
57
69
  try {
58
70
  git(`fetch origin ${sourceBranch} --depth=500`);
59
71
  } catch (err) {
60
- log(`[release-summary] fetch origin/${sourceBranch} failed: ${err.message}`);
72
+ log(
73
+ `[release-summary] fetch origin/${sourceBranch} failed: ${err.message}`,
74
+ );
61
75
  return [];
62
76
  }
63
77
 
64
78
  let untilArgs = '';
65
79
  try {
66
80
  const iso = git(`show -s --format=%cI ${to}`).trim();
67
- if (iso) untilArgs = ` --until=${shellQuote(iso)}`;
81
+ if (iso) {
82
+ untilArgs = ` --until=${shellQuote(iso)}`;
83
+ }
68
84
  } catch {
69
85
  // optional
70
86
  }
@@ -75,19 +91,26 @@ export function getCommitsFromSourceBranch(from, to, sourceBranch) {
75
91
  );
76
92
  return raw.trim().split('\n').filter(Boolean);
77
93
  } catch (err) {
78
- log(`[release-summary] git log ${from}..origin/${sourceBranch} failed: ${err.message}`);
94
+ log(
95
+ `[release-summary] git log ${from}..origin/${sourceBranch} failed: ${err.message}`,
96
+ );
79
97
  return [];
80
98
  }
81
99
  }
82
100
 
83
101
  export function getChangedPathsSummary(from, to) {
84
- if (!from || from === '0000000000000000000000000000000000000000') return '';
102
+ if (!from || from === '0000000000000000000000000000000000000000') {
103
+ return '';
104
+ }
85
105
  try {
86
106
  const raw = git(`diff --name-status --diff-filter=ACMR ${from}...${to}`);
87
107
  const lines = raw.trim().split('\n').filter(Boolean);
88
- if (!lines.length) return '';
108
+ if (!lines.length) {
109
+ return '';
110
+ }
89
111
  const shown = lines.slice(0, 80);
90
- const suffix = lines.length > 80 ? `\n…and ${lines.length - 80} more paths` : '';
112
+ const suffix =
113
+ lines.length > 80 ? `\n…and ${lines.length - 80} more paths` : '';
91
114
  return `${shown.join('\n')}${suffix}`;
92
115
  } catch {
93
116
  return '';
@@ -98,23 +121,32 @@ function fallbackMessage(project, branch, commits) {
98
121
  const header = `:rocket: *${project}* deployed to *${branch}*`;
99
122
  const useful = filterNoiseCommits(commits);
100
123
  const list = useful.length ? useful : commits;
101
- if (!list.length) return header;
102
- const bullets = list.slice(0, 10).map((c) => `• ${escapeSlack(c)}`).join('\n');
124
+ if (!list.length) {
125
+ return header;
126
+ }
127
+ const bullets = list
128
+ .slice(0, 10)
129
+ .map(c => `• ${escapeSlack(c)}`)
130
+ .join('\n');
103
131
  const suffix = list.length > 10 ? `\n_…and ${list.length - 10} more_` : '';
104
132
  return `${header}\n\n${bullets}${suffix}`;
105
133
  }
106
134
 
107
135
  async function getAccessToken(credentials) {
108
136
  const now = Math.floor(Date.now() / 1000);
109
- const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
110
- const payload = Buffer.from(JSON.stringify({
111
- iss: credentials.client_email,
112
- sub: credentials.client_email,
113
- aud: 'https://oauth2.googleapis.com/token',
114
- iat: now,
115
- exp: now + 3600,
116
- scope: 'https://www.googleapis.com/auth/cloud-platform',
117
- })).toString('base64url');
137
+ const header = Buffer.from(
138
+ JSON.stringify({ alg: 'RS256', typ: 'JWT' }),
139
+ ).toString('base64url');
140
+ const payload = Buffer.from(
141
+ JSON.stringify({
142
+ aud: 'https://oauth2.googleapis.com/token',
143
+ exp: now + 3600,
144
+ iat: now,
145
+ iss: credentials.client_email,
146
+ scope: 'https://www.googleapis.com/auth/cloud-platform',
147
+ sub: credentials.client_email,
148
+ }),
149
+ ).toString('base64url');
118
150
 
119
151
  const sign = createSign('RSA-SHA256');
120
152
  sign.update(`${header}.${payload}`);
@@ -122,11 +154,13 @@ async function getAccessToken(credentials) {
122
154
  const jwt = `${header}.${payload}.${signature}`;
123
155
 
124
156
  const res = await fetch('https://oauth2.googleapis.com/token', {
125
- method: 'POST',
126
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
127
157
  body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
158
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
159
+ method: 'POST',
128
160
  });
129
- if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
161
+ if (!res.ok) {
162
+ throw new Error(`Token exchange failed: ${res.status}`);
163
+ }
130
164
  const data = await res.json();
131
165
  return data.access_token;
132
166
  }
@@ -155,35 +189,39 @@ Rules:
155
189
  - If all commits are internal-only, write: "• Internal improvements and maintenance"
156
190
  - Do NOT include a header or footer — just the bullet points
157
191
  - Do NOT use Slack formatting like *bold* — plain text only
158
- - Ignore generic release titles like "release: deploy develop to production"
192
+ - Ignore generic release titles like "release: deploy main to production"
159
193
 
160
194
  Commits:
161
195
  ${commitList}${pathsBlock}`;
162
196
 
163
197
  const body = {
164
- contents: [{ role: 'user', parts: [{ text: prompt }] }],
198
+ contents: [{ parts: [{ text: prompt }], role: 'user' }],
165
199
  generationConfig: {
166
- temperature: 0.3,
167
200
  maxOutputTokens: 1024,
168
- topP: 0.8,
201
+ temperature: 0.3,
169
202
  thinkingConfig: { thinkingBudget: 0 },
203
+ topP: 0.8,
170
204
  },
171
205
  };
172
206
 
173
207
  const res = await fetch(endpoint, {
174
- method: 'POST',
208
+ body: JSON.stringify(body),
175
209
  headers: {
176
210
  Authorization: `Bearer ${accessToken}`,
177
211
  'Content-Type': 'application/json',
178
212
  },
179
- body: JSON.stringify(body),
213
+ method: 'POST',
180
214
  signal: AbortSignal.timeout(15_000),
181
215
  });
182
216
 
183
- if (!res.ok) throw new Error(`Gemini API error: ${res.status}`);
217
+ if (!res.ok) {
218
+ throw new Error(`Gemini API error: ${res.status}`);
219
+ }
184
220
  const data = await res.json();
185
221
  const text = data?.candidates?.[0]?.content?.parts?.[0]?.text;
186
- if (!text) throw new Error('Empty Gemini response');
222
+ if (!text) {
223
+ throw new Error('Empty Gemini response');
224
+ }
187
225
  return text.trim();
188
226
  }
189
227
 
@@ -198,19 +236,25 @@ export async function releaseSummary() {
198
236
  const toSha = env('CI_COMMIT_SHA', 'HEAD');
199
237
  const branch = env('CI_COMMIT_REF_NAME', 'unknown');
200
238
  const project = env('CI_PROJECT_NAME', 'app');
201
- const sourceBranch = env('RELEASE_SUMMARY_SOURCE_BRANCH', 'develop');
239
+ const sourceBranch = env('RELEASE_SUMMARY_SOURCE_BRANCH', 'main');
202
240
  const googleCreds = env('GOOGLE_CREDENTIALS_JSON');
203
241
 
204
242
  let commits = getCommitsInRange(fromSha, toSha);
205
243
 
206
244
  if (shouldResolveFromSourceBranch(commits)) {
207
- log(`[release-summary] squash release detected; resolving via origin/${sourceBranch}`);
245
+ log(
246
+ `[release-summary] squash release detected; resolving via origin/${sourceBranch}`,
247
+ );
208
248
  const fromSource = getCommitsFromSourceBranch(fromSha, toSha, sourceBranch);
209
- if (fromSource.length) commits = fromSource;
249
+ if (fromSource.length) {
250
+ commits = fromSource;
251
+ }
210
252
  }
211
253
 
212
254
  const filtered = filterNoiseCommits(commits);
213
- if (filtered.length) commits = filtered;
255
+ if (filtered.length) {
256
+ commits = filtered;
257
+ }
214
258
 
215
259
  log(`[release-summary] resolved ${commits.length} commit(s)`);
216
260