@waron97/prbot 3.1.1 → 3.1.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waron97/prbot",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,10 +8,12 @@ import { fuzzyMatch } from '../lib/fuzzy.js';
8
8
  import { execGit } from '../lib/git.js';
9
9
  import {
10
10
  appendPrToLine,
11
+ appendRefsToLine,
11
12
  buildRefString,
12
13
  detectIndentation,
13
14
  extractSections,
14
15
  findDuplicateLine,
16
+ findLineByPrNumber,
15
17
  findSectionEndLine,
16
18
  } from './changelog.js';
17
19
 
@@ -70,6 +72,28 @@ function buildPrDescription(taskIds, jiras) {
70
72
  return lines.join('\n');
71
73
  }
72
74
 
75
+ async function fetchActivePr(branch) {
76
+ const { DEVOPS_ORG, DEVOPS_PROJECT, DEVOPS_REPO } = process.env;
77
+ const apiUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_apis/git/repositories/${DEVOPS_REPO}/pullrequests?searchCriteria.sourceRefName=refs/heads/${branch}&searchCriteria.status=active&api-version=7.0`;
78
+ const res = await fetch(apiUrl, { headers: devopsHeaders() });
79
+ const data = await res.json();
80
+ if (!res.ok) throw new Error(`DevOps PR fetch failed: ${JSON.stringify(data)}`);
81
+ if (!data.value || data.value.length === 0) throw new Error(`No active PR found for branch: ${branch}`);
82
+ return data.value[0];
83
+ }
84
+
85
+ async function patchDevopsPrDescription(prId, description) {
86
+ const { DEVOPS_ORG, DEVOPS_PROJECT, DEVOPS_REPO } = process.env;
87
+ const apiUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_apis/git/repositories/${DEVOPS_REPO}/pullrequests/${prId}?api-version=7.0`;
88
+ const res = await fetch(apiUrl, {
89
+ method: 'PATCH',
90
+ headers: devopsHeaders(),
91
+ body: JSON.stringify({ description }),
92
+ });
93
+ const data = await res.json();
94
+ if (!res.ok) throw new Error(`DevOps PR patch failed: ${JSON.stringify(data)}`);
95
+ }
96
+
73
97
  async function createDevopsPR(branch, title, description) {
74
98
  const { DEVOPS_ORG, DEVOPS_PROJECT, DEVOPS_REPO, AUTOPR_TARGET_BRANCH } = process.env;
75
99
  const apiUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_apis/git/repositories/${DEVOPS_REPO}/pullrequests?api-version=7.0`;
@@ -190,7 +214,60 @@ async function selectSection(sections, candidates) {
190
214
  return sections.find((s) => s.heading === selected);
191
215
  }
192
216
 
217
+ async function autoprAmend(options) {
218
+ const ADDONS_PATH = resolveAddonsPath(process.env.ADDONS_PATH);
219
+ const changelogPath = `${ADDONS_PATH}/CHANGELOG.md`;
220
+ const { DEVOPS_ORG, DEVOPS_PROJECT, DEVOPS_REPO } = process.env;
221
+
222
+ const branch = (await execGit(['rev-parse', '--abbrev-ref', 'HEAD'], ADDONS_PATH)).trim();
223
+ const pr = await fetchActivePr(branch);
224
+ const prNumber = pr.pullRequestId;
225
+ const prUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_git/${DEVOPS_REPO}/pullrequest/${prNumber}`;
226
+ console.log(`Found PR #${prNumber}: ${prUrl}`);
227
+
228
+ const ids = options.trident ?? [];
229
+ const jiras = options.jira ?? [];
230
+
231
+ const tasks = ids.length > 0 ? await Promise.all(ids.map(fetchTask)) : [];
232
+ tasks.forEach((t) => console.log(`Task: ${t.name}`));
233
+
234
+ const newLinks = [
235
+ ...ids.map((id) => `${process.env.TRIDENT_URL}/odoo/my-tasks/${id}`),
236
+ ...jiras.map((j) => `https://sorgenia.atlassian.net/browse/${j}`),
237
+ ];
238
+ if (newLinks.length > 0) {
239
+ const updatedDescription = pr.description
240
+ ? `${pr.description}\n${newLinks.join('\n')}`
241
+ : newLinks.join('\n');
242
+ await patchDevopsPrDescription(prNumber, updatedDescription);
243
+ console.log('PR description updated');
244
+ }
245
+
246
+ for (let i = 0; i < ids.length; i++) {
247
+ await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl, prNumber);
248
+ }
249
+ if (ids.length > 0) console.log('Checklist updated');
250
+
251
+ const content = readFileSync(changelogPath, 'utf-8');
252
+ const existing = findLineByPrNumber(content, prNumber);
253
+ if (!existing) {
254
+ console.log(`Warning: no changelog line found for PR #${prNumber} — skipping changelog update`);
255
+ return;
256
+ }
257
+ const lines = content.split('\n');
258
+ lines[existing.lineNumber] = appendRefsToLine(existing.line, ids, jiras);
259
+ await fs.writeFile(changelogPath, lines.join('\n'));
260
+
261
+ await execGit(['add', 'CHANGELOG.md'], ADDONS_PATH);
262
+ await execGit(['commit', '-m', '[DOC][CHANGELOG] Changelog'], ADDONS_PATH);
263
+ await execGit(['push'], ADDONS_PATH);
264
+ console.log('Changelog updated and pushed');
265
+ console.log('\nReminder: squash the two changelog commits before merging the PR.');
266
+ }
267
+
193
268
  async function autopr(options) {
269
+ if (options.amend) return autoprAmend(options);
270
+
194
271
  const ADDONS_PATH = resolveAddonsPath(process.env.ADDONS_PATH);
195
272
  const changelogPath = `${ADDONS_PATH}/CHANGELOG.md`;
196
273
 
@@ -179,6 +179,54 @@ async function changelog(prNumber, options) {
179
179
  console.log('Changelog entry added');
180
180
  }
181
181
 
182
+ function findLineByPrNumber(content, prNumber) {
183
+ const lines = content.split('\n');
184
+ for (let i = 0; i < lines.length; i++) {
185
+ if (lines[i].includes(`#${prNumber}ADO`)) {
186
+ return { lineNumber: i, line: lines[i] };
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+
192
+ function appendRefsToLine(line, tridentIds, jiras) {
193
+ let result = line;
194
+
195
+ if (tridentIds && tridentIds.length > 0) {
196
+ const suffix = tridentIds.map((id) => `#${id}`).join(', ');
197
+ const tridentMatch = result.match(/Trident (#[\d, #]+)/);
198
+ if (tridentMatch) {
199
+ result = result.replace(/Trident (#[\d, #]+)/, `Trident ${tridentMatch[1]}, ${suffix}`);
200
+ } else {
201
+ const parenMatch = result.match(/\(([^)]*)\)\s*$/);
202
+ if (parenMatch) {
203
+ result = result.replace(/\(([^)]*)\)\s*$/, `(Trident ${suffix}, ${parenMatch[1]})`);
204
+ } else {
205
+ result = `${result.trimEnd()} (Trident ${suffix})`;
206
+ }
207
+ }
208
+ }
209
+
210
+ if (jiras && jiras.length > 0) {
211
+ const suffix = jiras.join(', ');
212
+ const jiraMatch = result.match(/JIRA ([^,)]+)/);
213
+ if (jiraMatch) {
214
+ result = result.replace(/JIRA ([^,)]+)/, `JIRA ${jiraMatch[1].trimEnd()}, ${suffix}`);
215
+ } else if (result.includes('PR sorgenia_addons')) {
216
+ result = result.replace(/(PR sorgenia_addons)/, `JIRA ${suffix}, $1`);
217
+ } else {
218
+ const parenMatch = result.match(/\(([^)]*)\)\s*$/);
219
+ if (parenMatch) {
220
+ result = result.replace(/\(([^)]*)\)\s*$/, `(${parenMatch[1]}, JIRA ${suffix})`);
221
+ } else {
222
+ result = `${result.trimEnd()} (JIRA ${suffix})`;
223
+ }
224
+ }
225
+ }
226
+
227
+ return result;
228
+ }
229
+
182
230
  export {
183
231
  changelog,
184
232
  extractSections,
@@ -187,4 +235,6 @@ export {
187
235
  buildRefString,
188
236
  findDuplicateLine,
189
237
  appendPrToLine,
238
+ findLineByPrNumber,
239
+ appendRefsToLine,
190
240
  };
@@ -169,7 +169,7 @@ async function exportEmailTemplates(opts) {
169
169
  log(`Written: ${outPath}`);
170
170
 
171
171
  const newMappings = await readEmailTemplateMappings(dataDir);
172
- const renamedCodes = detectEmailRenames(oldMappings, newMappings);
172
+ const renames = detectEmailRenames(oldMappings, newMappings);
173
173
 
174
174
  let bumpLevel = opts.bump;
175
175
  if (!bumpLevel) {
@@ -186,13 +186,13 @@ async function exportEmailTemplates(opts) {
186
186
 
187
187
  let preMigratePath = null;
188
188
 
189
- if (renamedCodes.length > 0) {
190
- log(`Renamed template_codes (${renamedCodes.length}): ${renamedCodes.join(', ')}`);
189
+ if (renames.length > 0) {
190
+ log(`Renamed XML IDs (${renames.length}): ${renames.map((r) => `${r.oldXmlId} → ${r.newXmlId}`).join(', ')}`);
191
191
 
192
192
  let shouldGenerate = opts.autoPremigrate;
193
193
  if (!shouldGenerate && !isSilent()) {
194
194
  shouldGenerate = await confirm({
195
- message: `Detected ${renamedCodes.length} renamed template_code(s). Generate pre-migrate script?`,
195
+ message: `Detected ${renames.length} renamed template_code(s). Generate pre-migrate script?`,
196
196
  default: true,
197
197
  });
198
198
  }
@@ -206,7 +206,7 @@ async function exportEmailTemplates(opts) {
206
206
  const migrationDir = path.join(ADDONS_PATH, 'config', module, 'migrations', version);
207
207
  preMigratePath = path.join(migrationDir, 'pre-migrate.py');
208
208
  await fs.mkdir(migrationDir, { recursive: true });
209
- await fs.writeFile(preMigratePath, generateEmailPreMigrateScript(renamedCodes));
209
+ await fs.writeFile(preMigratePath, generateEmailPreMigrateScript(renames));
210
210
  log(`Wrote pre-migrate: ${preMigratePath}`);
211
211
  }
212
212
  }
package/src/index.js CHANGED
@@ -77,6 +77,7 @@ program
77
77
  .option('-m, --message <text>', 'Changelog entry message')
78
78
  .option('-b, --branch <name>', 'Branch name (default: autopr_<taskId>)')
79
79
  .option('-n, --name <text>', 'PR title (default: task name from Odoo)')
80
+ .option('--amend', 'Amend existing PR on current branch with new trident/jira refs')
80
81
  .action((opts) => {
81
82
  autopr(opts).catch((err) => {
82
83
  throw err;
@@ -167,15 +167,15 @@ async function readEmailTemplateMappings(dataDir) {
167
167
  }
168
168
 
169
169
  function detectEmailRenames(oldMap, newMap) {
170
- const templateCodes = [];
170
+ const renames = [];
171
171
  for (const [code, oldXmlId] of oldMap) {
172
172
  const newXmlId = newMap.get(code);
173
- if (newXmlId && newXmlId !== oldXmlId) templateCodes.push(code);
173
+ if (newXmlId && newXmlId !== oldXmlId) renames.push({ oldXmlId, newXmlId });
174
174
  }
175
- return templateCodes;
175
+ return renames;
176
176
  }
177
177
 
178
- function generateEmailPreMigrateScript(templateCodes) {
178
+ function generateEmailPreMigrateScript(renames) {
179
179
  const lines = [
180
180
  '# Copyright 2025-TODAY Symphonie Prime S.r.l. (www.symphonieprime.com)',
181
181
  '# All rights reserved.',
@@ -185,28 +185,20 @@ function generateEmailPreMigrateScript(templateCodes) {
185
185
  '',
186
186
  '@openupgrade.migrate()',
187
187
  'def migrate(env, version):',
188
- ' env.cr.execute(',
189
- ' "SELECT 1 FROM information_schema.tables WHERE table_name = %s",',
190
- ' ("mail_template",),',
191
- ' )',
192
- ' if not env.cr.fetchone():',
193
- ' return',
194
- '',
188
+ ' renames = [',
195
189
  ];
196
190
 
197
- if (templateCodes.length === 1) {
198
- lines.push(` template_codes = ("${templateCodes[0]}",)`);
199
- } else {
200
- lines.push(' template_codes = (');
201
- for (const code of templateCodes) lines.push(` "${code}",`);
202
- lines.push(' )');
191
+ for (const { oldXmlId, newXmlId } of renames) {
192
+ lines.push(` ("${oldXmlId}", "${newXmlId}"),`);
203
193
  }
204
194
 
205
195
  lines.push(
206
- ' env.cr.execute(',
207
- ' "DELETE FROM mail_template WHERE template_code IN %s",',
208
- ' (template_codes,),',
209
- ' )',
196
+ ' ]',
197
+ ' for old_name, new_name in renames:',
198
+ ' env.cr.execute(',
199
+ ' "UPDATE ir_model_data SET name = %s WHERE name = %s AND model = %s",',
200
+ ' (new_name, old_name, "mail.template"),',
201
+ ' )',
210
202
  );
211
203
 
212
204
  return lines.join('\n') + '\n';