@waron97/prbot 3.3.0 → 3.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.
- package/README.md +53 -37
- package/package.json +5 -1
- package/src/agrippa/commands/clone.js +10 -9
- package/src/agrippa/commands/cloneLrp.js +9 -9
- package/src/agrippa/commands/clonePb.js +10 -10
- package/src/agrippa/commands/diff.js +64 -25
- package/src/agrippa/commands/init.js +14 -13
- package/src/agrippa/commands/initPhase.js +10 -11
- package/src/agrippa/commands/pb.js +29 -26
- package/src/agrippa/commands/pull.js +67 -48
- package/src/agrippa/commands/pullLrp.js +2 -3
- package/src/agrippa/commands/pullPb.js +2 -3
- package/src/agrippa/commands/push.js +33 -24
- package/src/agrippa/commands/repair.js +4 -3
- package/src/agrippa/index.js +25 -13
- package/src/agrippa/lib/pbEdit.js +49 -2
- package/src/agrippa/lib/pbLayout.js +8 -1
- package/src/agrippa/lib/pbModel.js +1 -0
- package/src/agrippa/lib/pbPreview.js +6 -2
- package/src/agrippa/lib/pbProject.js +91 -3
- package/src/commands/autopr.js +20 -21
- package/src/commands/changelog.js +4 -3
- package/src/commands/commit.js +11 -10
- package/src/commands/export.js +2 -1
- package/src/commands/exportPb.js +19 -2
- package/src/commands/init.js +2 -1
- package/src/commands/routine.js +19 -8
- package/src/index.js +137 -112
- package/src/lib/auth.js +19 -1
- package/src/lib/logger.js +12 -1
|
@@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { getToken } from '../../lib/auth.js';
|
|
5
|
+
import { log } from '../../lib/logger.js';
|
|
5
6
|
import { updateMfa, updatePhase } from '../lib/api.js';
|
|
6
7
|
import { computeChecksum } from '../lib/checksum.js';
|
|
7
8
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
@@ -23,24 +24,28 @@ async function push(opts = {}) {
|
|
|
23
24
|
// Stale-entry check (a path may be a file for phase/mfa or a dir for pb).
|
|
24
25
|
const stale = config.workspace.filter((e) => !fileExists(e.path));
|
|
25
26
|
if (stale.length) {
|
|
26
|
-
|
|
27
|
-
stale.forEach((e) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
log('\nThe following tracked resources no longer exist on disk:');
|
|
28
|
+
stale.forEach((e) => log(` - ${e.path} (${e.name})`));
|
|
29
|
+
if (opts.nonInteractive) {
|
|
30
|
+
log(' (non-interactive: leaving them tracked; run interactively to clean up)');
|
|
31
|
+
} else {
|
|
32
|
+
const { cleanup } = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'confirm',
|
|
35
|
+
name: 'cleanup',
|
|
36
|
+
message: 'Remove these entries from the workspace config?',
|
|
37
|
+
default: true,
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
if (cleanup) {
|
|
41
|
+
config.workspace = config.workspace.filter((e) => fileExists(e.path));
|
|
42
|
+
writeConfig(config);
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
if (!config.workspace.length) {
|
|
43
|
-
|
|
48
|
+
log('No tracked resources. Run `agrippa clone` first.');
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -59,7 +64,7 @@ async function push(opts = {}) {
|
|
|
59
64
|
'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
|
|
60
65
|
);
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
log('Fetching remote state...');
|
|
63
68
|
const token = await getToken();
|
|
64
69
|
|
|
65
70
|
const remoteCodeMap = hasCode
|
|
@@ -123,13 +128,13 @@ async function push(opts = {}) {
|
|
|
123
128
|
|
|
124
129
|
const changed = classified.filter((e) => e.status !== 'unchanged');
|
|
125
130
|
if (!changed.length) {
|
|
126
|
-
|
|
131
|
+
log('Nothing to push — everything matches the last-pulled state.');
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
const selected = await selectEntries(changed, 'push (overwrites remote)',
|
|
135
|
+
const selected = await selectEntries(changed, 'push (overwrites remote)', opts);
|
|
131
136
|
if (!selected.length) {
|
|
132
|
-
|
|
137
|
+
log('Nothing selected. No changes made.');
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
135
140
|
|
|
@@ -168,7 +173,7 @@ async function push(opts = {}) {
|
|
|
168
173
|
]
|
|
169
174
|
.filter(Boolean)
|
|
170
175
|
.join(', ');
|
|
171
|
-
|
|
176
|
+
log(` ${entry.name} → saved (draft)${note ? ` [${note}]` : ''}`);
|
|
172
177
|
if (idx !== -1) {
|
|
173
178
|
config.workspace[idx].checksum_at_pull = res.newChecksum;
|
|
174
179
|
if (res.newUpdatedDate) config.workspace[idx].updated_date = res.newUpdatedDate;
|
|
@@ -177,7 +182,7 @@ async function push(opts = {}) {
|
|
|
177
182
|
pushedPb.push({ entry, idx });
|
|
178
183
|
} else if (entry.object_type === 'long_running_process') {
|
|
179
184
|
const res = await pushLrpEntry(token, entry, BACKUP_DIR, backupTs);
|
|
180
|
-
|
|
185
|
+
log(` ${entry.name} → saved`);
|
|
181
186
|
if (idx !== -1) {
|
|
182
187
|
config.workspace[idx].checksum_at_pull = res.newChecksum;
|
|
183
188
|
config.workspace[idx].tenant_id = res.newRow.tenantId;
|
|
@@ -197,7 +202,7 @@ async function push(opts = {}) {
|
|
|
197
202
|
pushed++;
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
|
|
205
|
+
log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
|
|
201
206
|
|
|
202
207
|
// Publish/deploy step for pushed wizards and LRPs.
|
|
203
208
|
if (pushedPb.length) {
|
|
@@ -208,7 +213,7 @@ async function push(opts = {}) {
|
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
writeConfig(config);
|
|
211
|
-
|
|
216
|
+
log(`\nPushed ${pushed} record(s).`);
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
async function handlePublish(token, pushedPb, config, opts) {
|
|
@@ -216,6 +221,8 @@ async function handlePublish(token, pushedPb, config, opts) {
|
|
|
216
221
|
let doPublish;
|
|
217
222
|
if (opts.skipPublish) doPublish = false;
|
|
218
223
|
else if (opts.publish) doPublish = true;
|
|
224
|
+
else if (opts.nonInteractive)
|
|
225
|
+
doPublish = false; // no stdin to prompt on; default to safe
|
|
219
226
|
else {
|
|
220
227
|
({ doPublish } = await inquirer.prompt([
|
|
221
228
|
{
|
|
@@ -229,7 +236,7 @@ async function handlePublish(token, pushedPb, config, opts) {
|
|
|
229
236
|
if (doPublish) {
|
|
230
237
|
await publish(token, entry.guid);
|
|
231
238
|
if (idx !== -1) config.workspace[idx].status = 'published';
|
|
232
|
-
|
|
239
|
+
log(` published ${entry.name}`);
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
242
|
}
|
|
@@ -239,6 +246,8 @@ async function handleDeploy(token, pushedLrp, config, opts) {
|
|
|
239
246
|
let doDeploy;
|
|
240
247
|
if (opts.skipPublish) doDeploy = false;
|
|
241
248
|
else if (opts.publish) doDeploy = true;
|
|
249
|
+
else if (opts.nonInteractive)
|
|
250
|
+
doDeploy = false; // no stdin to prompt on; default to safe
|
|
242
251
|
else {
|
|
243
252
|
({ doDeploy } = await inquirer.prompt([
|
|
244
253
|
{
|
|
@@ -252,7 +261,7 @@ async function handleDeploy(token, pushedLrp, config, opts) {
|
|
|
252
261
|
if (doDeploy) {
|
|
253
262
|
await deploy(token, deployId);
|
|
254
263
|
if (idx !== -1) config.workspace[idx].status = 'deployed';
|
|
255
|
-
|
|
264
|
+
log(` deployed ${entry.name}`);
|
|
256
265
|
}
|
|
257
266
|
}
|
|
258
267
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../../lib/logger.js';
|
|
1
2
|
import { readConfig, writeConfig } from '../lib/config.js';
|
|
2
3
|
import { fileExists } from '../lib/workspace.js';
|
|
3
4
|
|
|
@@ -9,16 +10,16 @@ async function repair() {
|
|
|
9
10
|
config.workspace = config.workspace.filter((entry) => fileExists(entry.path));
|
|
10
11
|
|
|
11
12
|
if (!stale.length) {
|
|
12
|
-
|
|
13
|
+
log('No stale entries found.');
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
for (const entry of stale) {
|
|
17
|
-
|
|
18
|
+
log(` removed: ${entry.path} (${entry.name})`);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
writeConfig(config);
|
|
21
|
-
|
|
22
|
+
log(
|
|
22
23
|
`Removed ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'} (${before} → ${config.workspace.length}).`
|
|
23
24
|
);
|
|
24
25
|
}
|
package/src/agrippa/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { program } from 'commander';
|
|
4
|
+
import { error } from '../lib/logger.js';
|
|
4
5
|
import { clone } from './commands/clone.js';
|
|
5
6
|
import { diff } from './commands/diff.js';
|
|
6
7
|
import { init } from './commands/init.js';
|
|
@@ -24,7 +25,7 @@ const require = createRequire(import.meta.url);
|
|
|
24
25
|
const { version } = require('../../package.json');
|
|
25
26
|
|
|
26
27
|
process.on('unhandledRejection', (err) => {
|
|
27
|
-
|
|
28
|
+
error(`Error: ${err.message}`);
|
|
28
29
|
process.exit(1);
|
|
29
30
|
});
|
|
30
31
|
|
|
@@ -35,7 +36,7 @@ program
|
|
|
35
36
|
.description('Create agrippa.yaml workspace config in the current directory')
|
|
36
37
|
.action(() =>
|
|
37
38
|
init().catch((err) => {
|
|
38
|
-
|
|
39
|
+
error(`Error: ${err.message}`);
|
|
39
40
|
process.exit(1);
|
|
40
41
|
})
|
|
41
42
|
);
|
|
@@ -54,7 +55,7 @@ program
|
|
|
54
55
|
.option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb/lrp)')
|
|
55
56
|
.action((opts) =>
|
|
56
57
|
clone(opts).catch((err) => {
|
|
57
|
-
|
|
58
|
+
error(`Error: ${err.message}`);
|
|
58
59
|
process.exit(1);
|
|
59
60
|
})
|
|
60
61
|
);
|
|
@@ -62,9 +63,13 @@ program
|
|
|
62
63
|
program
|
|
63
64
|
.command('pull')
|
|
64
65
|
.description('Pull remote changes into local files')
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
.option(
|
|
67
|
+
'--non-interactive',
|
|
68
|
+
'No prompts; auto-select safe (fast-forward) entries and fail if any is in conflict'
|
|
69
|
+
)
|
|
70
|
+
.action((opts) =>
|
|
71
|
+
pull(opts).catch((err) => {
|
|
72
|
+
error(`Error: ${err.message}`);
|
|
68
73
|
process.exit(1);
|
|
69
74
|
})
|
|
70
75
|
);
|
|
@@ -74,19 +79,26 @@ program
|
|
|
74
79
|
.description('Push local changes to RIP / Process Builder / LRP (backs up remote first)')
|
|
75
80
|
.option('--publish', 'Auto-publish pushed wizards and auto-deploy pushed LRPs')
|
|
76
81
|
.option('--skip-publish', 'Skip publishing/deploying pushed wizards and LRPs (no prompt)')
|
|
82
|
+
.option(
|
|
83
|
+
'--non-interactive',
|
|
84
|
+
'No prompts; auto-select safe (fast-forward) entries and fail if any is in conflict'
|
|
85
|
+
)
|
|
77
86
|
.action((opts) =>
|
|
78
87
|
push(opts).catch((err) => {
|
|
79
|
-
|
|
88
|
+
error(`Error: ${err.message}`);
|
|
80
89
|
process.exit(1);
|
|
81
90
|
})
|
|
82
91
|
);
|
|
83
92
|
|
|
84
93
|
program
|
|
85
|
-
.command('diff [
|
|
86
|
-
.description(
|
|
94
|
+
.command('diff [target]')
|
|
95
|
+
.description(
|
|
96
|
+
'Show differences between local files and remote code. [target] = file, folder, ' +
|
|
97
|
+
'project dir, document_id or name; omit for the whole workspace'
|
|
98
|
+
)
|
|
87
99
|
.action((path) =>
|
|
88
100
|
diff(path).catch((err) => {
|
|
89
|
-
|
|
101
|
+
error(`Error: ${err.message}`);
|
|
90
102
|
process.exit(1);
|
|
91
103
|
})
|
|
92
104
|
);
|
|
@@ -96,7 +108,7 @@ program
|
|
|
96
108
|
.description('Initialize a phase with default code template and result vars')
|
|
97
109
|
.action(() =>
|
|
98
110
|
initPhase().catch((err) => {
|
|
99
|
-
|
|
111
|
+
error(`Error: ${err.message}`);
|
|
100
112
|
process.exit(1);
|
|
101
113
|
})
|
|
102
114
|
);
|
|
@@ -106,14 +118,14 @@ program
|
|
|
106
118
|
.description('Remove stale workspace entries where local file no longer exists')
|
|
107
119
|
.action(() =>
|
|
108
120
|
repair().catch((err) => {
|
|
109
|
-
|
|
121
|
+
error(`Error: ${err.message}`);
|
|
110
122
|
process.exit(1);
|
|
111
123
|
})
|
|
112
124
|
);
|
|
113
125
|
|
|
114
126
|
// ---- pb: local editing helpers for a cloned process-builder wizard or LRP ----
|
|
115
127
|
const die = (err) => {
|
|
116
|
-
|
|
128
|
+
error(`Error: ${err.message}`);
|
|
117
129
|
process.exit(1);
|
|
118
130
|
};
|
|
119
131
|
const pb = program
|
|
@@ -317,7 +317,13 @@ function removeNode(structure, manifest, { id }) {
|
|
|
317
317
|
);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
const clearedDefaults = clearDanglingDefaults(structure);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
writes: {},
|
|
324
|
+
deletes,
|
|
325
|
+
result: { removed: [...victimIds], removedEdges, clearedDefaults },
|
|
326
|
+
};
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
// ---------- connect / disconnect ----------
|
|
@@ -492,15 +498,55 @@ function lintIncomingEdges(structure) {
|
|
|
492
498
|
return issues;
|
|
493
499
|
}
|
|
494
500
|
|
|
501
|
+
// Flag `attrs.default` values that don't name an outgoing edge of their own
|
|
502
|
+
// node. lintGateways only covers the inverse (a gateway that needs a default
|
|
503
|
+
// and hasn't got a valid one), so a node left with a single outgoing flow and a
|
|
504
|
+
// default pointing at a deleted one used to pass clean — and then fail at
|
|
505
|
+
// publish with Activiti's null-reference NPE. The edits that delete edges now
|
|
506
|
+
// clear these themselves (clearDanglingDefaults); this rule is the net for
|
|
507
|
+
// hand-edited structure.yaml and for projects predating that fix.
|
|
508
|
+
function lintDanglingDefaults(structure) {
|
|
509
|
+
const issues = [];
|
|
510
|
+
eachNode(structure.nodes, null, (n) => {
|
|
511
|
+
const def = n.attrs?.default;
|
|
512
|
+
if (!def) return;
|
|
513
|
+
if ((n.edges || []).some((e) => e.id === def)) return;
|
|
514
|
+
issues.push(
|
|
515
|
+
`${n.id} (${n.name || n.type}): default flow ${def} does not exist on this node — ` +
|
|
516
|
+
`Activiti fails the deploy on the dangling reference.`
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
return issues;
|
|
520
|
+
}
|
|
521
|
+
|
|
495
522
|
// Run all lint rules and return combined issues.
|
|
496
523
|
function lintAll(structure) {
|
|
497
524
|
return [
|
|
498
525
|
...lintGateways(structure),
|
|
526
|
+
...lintDanglingDefaults(structure),
|
|
499
527
|
...lintEdgeNames(structure),
|
|
500
528
|
...lintIncomingEdges(structure),
|
|
501
529
|
];
|
|
502
530
|
}
|
|
503
531
|
|
|
532
|
+
// Drop every `attrs.default` that no longer names an outgoing edge of its own
|
|
533
|
+
// node. Must run after any edit that deletes edges: a default referencing a
|
|
534
|
+
// removed flow recomposes fine and passes the other lint rules, but Activiti
|
|
535
|
+
// resolves the reference at deploy time, gets null, and the publish fails with
|
|
536
|
+
// an unattributed `Could not deploy the XML: null. Error: null`. Returns the
|
|
537
|
+
// cleared [nodeId, edgeId] pairs so callers can report them.
|
|
538
|
+
function clearDanglingDefaults(structure) {
|
|
539
|
+
const cleared = [];
|
|
540
|
+
eachNode(structure.nodes, null, (n) => {
|
|
541
|
+
const def = n.attrs?.default;
|
|
542
|
+
if (!def) return;
|
|
543
|
+
if ((n.edges || []).some((e) => e.id === def)) return;
|
|
544
|
+
delete n.attrs.default;
|
|
545
|
+
cleared.push({ node: n.id, edge: def });
|
|
546
|
+
});
|
|
547
|
+
return cleared;
|
|
548
|
+
}
|
|
549
|
+
|
|
504
550
|
// Remove an edge by id, or by --from/--to pair.
|
|
505
551
|
function disconnect(structure, { id, from, to }) {
|
|
506
552
|
let removed = 0;
|
|
@@ -516,7 +562,8 @@ function disconnect(structure, { id, from, to }) {
|
|
|
516
562
|
removed += before - n.edges.length;
|
|
517
563
|
});
|
|
518
564
|
if (!removed) throw new Error(id ? `edge not found: ${id}` : `no edge from ${from} to ${to}`);
|
|
519
|
-
|
|
565
|
+
const clearedDefaults = clearDanglingDefaults(structure);
|
|
566
|
+
return { writes: {}, deletes: [], result: { removed, id: removedId, clearedDefaults } };
|
|
520
567
|
}
|
|
521
568
|
|
|
522
569
|
// ---------- list ----------
|
|
@@ -154,7 +154,14 @@ async function autoLayout(structure) {
|
|
|
154
154
|
targets: [e.target],
|
|
155
155
|
sourcePort,
|
|
156
156
|
labels: e.name
|
|
157
|
-
? [
|
|
157
|
+
? [
|
|
158
|
+
{
|
|
159
|
+
id: `${e.id}__lbl`,
|
|
160
|
+
text: e.name,
|
|
161
|
+
width: Math.min(e.name.length * 6, 140),
|
|
162
|
+
height: 14,
|
|
163
|
+
},
|
|
164
|
+
]
|
|
158
165
|
: [],
|
|
159
166
|
layoutOptions: {
|
|
160
167
|
'elk.layered.priority.straightness': isHappy ? '10' : 1,
|
|
@@ -92,8 +92,12 @@ function toSvg(structure) {
|
|
|
92
92
|
);
|
|
93
93
|
if (e.name) {
|
|
94
94
|
const lp = e.labelPos;
|
|
95
|
-
const lx = lp
|
|
96
|
-
|
|
95
|
+
const lx = lp
|
|
96
|
+
? lp.x + lp.width / 2
|
|
97
|
+
: e.waypoints[Math.floor(e.waypoints.length / 2)][0];
|
|
98
|
+
const ly = lp
|
|
99
|
+
? lp.y + lp.height / 2
|
|
100
|
+
: e.waypoints[Math.floor(e.waypoints.length / 2)][1] - 4;
|
|
97
101
|
out.push(
|
|
98
102
|
`<text x="${lx + ox}" y="${ly + oy}" text-anchor="middle" fill="#666" font-size="9">${esc(e.name)}</text>`
|
|
99
103
|
);
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
import slugify from 'slugify';
|
|
19
19
|
import { Document, visit, parse as yamlParse, stringify as yamlStringify } from 'yaml';
|
|
20
20
|
import { computeChecksum } from './checksum.js';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildProcess,
|
|
23
|
+
compareDiagram,
|
|
24
|
+
compareProcess,
|
|
25
|
+
diagramGeometry,
|
|
26
|
+
diff,
|
|
27
|
+
normalizeProcessTree,
|
|
28
|
+
parseProcess,
|
|
29
|
+
} from './pbModel.js';
|
|
22
30
|
|
|
23
31
|
const MANIFEST_FILE = '.agrippa-pb.json';
|
|
24
32
|
const STRUCTURE_FILE = 'structure.yaml';
|
|
@@ -355,10 +363,89 @@ function stableStringify(value) {
|
|
|
355
363
|
return JSON.stringify(value ?? null);
|
|
356
364
|
}
|
|
357
365
|
|
|
366
|
+
// Canonical form of a recomposed payload used ONLY for change-detection
|
|
367
|
+
// checksums — never for the actual push payload (recompose() itself, and
|
|
368
|
+
// what gets sent on push, are untouched). Routes built_page through the same
|
|
369
|
+
// formatting/order-insensitive canonicalization already used by
|
|
370
|
+
// comparePayload()'s 0-loss round-trip verification:
|
|
371
|
+
// - normalizeProcessTree drops insignificant whitespace text nodes (incl.
|
|
372
|
+
// the extraDefs `#text` padding between <definitions>-root siblings) and
|
|
373
|
+
// sorts declarations by id, so cosmetic reformatting doesn't count;
|
|
374
|
+
// - diagramGeometry keeps only x/y/width/height/isExpanded/waypoints, so
|
|
375
|
+
// `format`-only artifacts like labelPos never enter the comparison.
|
|
376
|
+
// Without this, a push followed by an unmodified re-fetch could classify as
|
|
377
|
+
// a phantom `conflict` instead of `unchanged` — see
|
|
378
|
+
// ai_tasks/2026-07-16-lrp-clone/deferred_work.md items 1, 3, 4.
|
|
379
|
+
//
|
|
380
|
+
// `updated_date`/`modified_by` are server-managed bookkeeping, not process
|
|
381
|
+
// content — Odoo/Symple bump them on any touch of the record, including ones
|
|
382
|
+
// with no semantic effect (observed live: an integration service account
|
|
383
|
+
// re-saving the wizard moved `updated_date`/`modified_by` on both the
|
|
384
|
+
// top-level payload and a page wrapper with zero content diff otherwise, via
|
|
385
|
+
// comparePayload). They exist at two levels: the top-level payload scalars,
|
|
386
|
+
// and per-entry inside `payload.pages` (each page's audit wrapper, distinct
|
|
387
|
+
// from the page *content* under `.page`, which is left untouched). Left in,
|
|
388
|
+
// they permanently pin the object to `conflict` the moment anything server-
|
|
389
|
+
// side touches it, regardless of actual content — worse than the cosmetic
|
|
390
|
+
// built_page diffs above, since nothing local or an agrippa push causes them
|
|
391
|
+
// to resync.
|
|
392
|
+
const VOLATILE_AUDIT_FIELDS = ['updated_date', 'modified_by'];
|
|
393
|
+
|
|
394
|
+
// `status`/`version` are the server's publication lifecycle, not content —
|
|
395
|
+
// excluded for the same reason as the audit fields above, but only at the top
|
|
396
|
+
// level (page wrappers carry neither). They surface in process.yaml via
|
|
397
|
+
// EDITABLE_SCALARS and are still sent on push; they just don't count as a
|
|
398
|
+
// change. Every PATCH flips the wizard server-side to draft/modified while
|
|
399
|
+
// process.yaml keeps whatever status was recorded at the last clone/pull, and
|
|
400
|
+
// push stores the *local* checksum as the new baseline — so a push not
|
|
401
|
+
// followed by a publish guaranteed a phantom `conflict` on the next push,
|
|
402
|
+
// with zero content difference on either side (observed live 2026-07-21 on
|
|
403
|
+
// ml_voltura_fibra_data_input: upstream came back `modified` against a
|
|
404
|
+
// `published` process.yaml, shifting the remote checksum off the baseline on
|
|
405
|
+
// its own). `version` is worse still: a publish that bumps it server-side
|
|
406
|
+
// pins the object to `conflict` permanently, since nothing local resyncs it.
|
|
407
|
+
const VOLATILE_LIFECYCLE_FIELDS = ['status', 'version'];
|
|
408
|
+
|
|
409
|
+
function canonicalForChecksum(payload) {
|
|
410
|
+
const { process, decls } = normalizeProcessTree(payload.built_page);
|
|
411
|
+
const rest = omit(payload, [
|
|
412
|
+
'built_page',
|
|
413
|
+
'pages',
|
|
414
|
+
...VOLATILE_AUDIT_FIELDS,
|
|
415
|
+
...VOLATILE_LIFECYCLE_FIELDS,
|
|
416
|
+
]);
|
|
417
|
+
// Page order is not a stable identity — locally it's whatever order the
|
|
418
|
+
// manifest happened to record at last decompose, while a live upstream
|
|
419
|
+
// fetch can return the same set of pages in a different order (observed
|
|
420
|
+
// live: a fresh fetch came back with pages 1-3 cyclically rotated versus
|
|
421
|
+
// the local manifest, same guids, zero content difference). Sort by the
|
|
422
|
+
// page's own guid — stable, unlike array position — before hashing, the
|
|
423
|
+
// same trick normalizeProcessTree already uses for extraDefs decls.
|
|
424
|
+
const pages = [...(payload.pages || [])]
|
|
425
|
+
.sort((a, b) => (a.guid ?? '').localeCompare(b.guid ?? ''))
|
|
426
|
+
.map((p) => omit(p, VOLATILE_AUDIT_FIELDS));
|
|
427
|
+
return {
|
|
428
|
+
...rest,
|
|
429
|
+
pages,
|
|
430
|
+
built_page_process: process,
|
|
431
|
+
built_page_decls: decls,
|
|
432
|
+
built_page_diagram: diagramGeometry(payload.built_page),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Checksum of a recomposed payload, canonicalized so cosmetic round-trip
|
|
437
|
+
// differences don't register as a change. Used for checksum_at_pull baselines
|
|
438
|
+
// (clone/pull) and for the push/pull classifier (localChecksum/
|
|
439
|
+
// remoteChecksumPb below) — keep every PB/LRP checksum call site on this one
|
|
440
|
+
// function so baselines and comparisons stay on the same normalization.
|
|
441
|
+
function checksumOfPayload(payload) {
|
|
442
|
+
return computeChecksum(stableStringify(canonicalForChecksum(payload)));
|
|
443
|
+
}
|
|
444
|
+
|
|
358
445
|
// Checksum of the recomposed payload — stable across runs, changes only when the
|
|
359
446
|
// local files change. clonePb stores this as checksum_at_pull; push recomputes it.
|
|
360
447
|
function localChecksum(read) {
|
|
361
|
-
return
|
|
448
|
+
return checksumOfPayload(recompose(read));
|
|
362
449
|
}
|
|
363
450
|
|
|
364
451
|
// Semantic checksum of a remote payload. Decompose → recompose normalises field
|
|
@@ -367,7 +454,7 @@ function localChecksum(read) {
|
|
|
367
454
|
function remoteChecksumPb(payload) {
|
|
368
455
|
const { files } = decompose(payload);
|
|
369
456
|
const read = (p) => files[p] ?? '';
|
|
370
|
-
return
|
|
457
|
+
return checksumOfPayload(recompose(read));
|
|
371
458
|
}
|
|
372
459
|
|
|
373
460
|
// Enumerate the wizard's pages from the pages/*.yml files (the authoritative
|
|
@@ -406,6 +493,7 @@ export {
|
|
|
406
493
|
comparePayload,
|
|
407
494
|
verifyRoundTrip,
|
|
408
495
|
stableStringify,
|
|
496
|
+
checksumOfPayload,
|
|
409
497
|
localChecksum,
|
|
410
498
|
remoteChecksumPb,
|
|
411
499
|
enumeratePages,
|
package/src/commands/autopr.js
CHANGED
|
@@ -6,6 +6,7 @@ import fetch from 'node-fetch';
|
|
|
6
6
|
import { resolveAddonsPath } from '../lib/addons.js';
|
|
7
7
|
import { fuzzyMatch } from '../lib/fuzzy.js';
|
|
8
8
|
import { execGit } from '../lib/git.js';
|
|
9
|
+
import { log } from '../lib/logger.js';
|
|
9
10
|
import {
|
|
10
11
|
appendPrToLine,
|
|
11
12
|
appendRefsToLine,
|
|
@@ -120,7 +121,7 @@ async function createDevopsPR(branch, title, description) {
|
|
|
120
121
|
return { id: data.pullRequestId, url: prUrl };
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
async function appendChecklistPrLink(taskId, currentChecklist, prUrl
|
|
124
|
+
async function appendChecklistPrLink(taskId, currentChecklist, prUrl) {
|
|
124
125
|
const link = `<a href="${prUrl}">${prUrl}</a><br/>`;
|
|
125
126
|
const updated = currentChecklist ? `${currentChecklist}\n${link}` : link;
|
|
126
127
|
const url = `${process.env.TRIDENT_URL}/jsonrpc`;
|
|
@@ -191,8 +192,8 @@ async function selectSection(sections, candidates) {
|
|
|
191
192
|
const scored = candidates.length > 0 ? scoreSections(sections, candidates) : [];
|
|
192
193
|
|
|
193
194
|
if (candidates.length > 0) {
|
|
194
|
-
|
|
195
|
-
if (scored.length === 0)
|
|
195
|
+
log(`\nTask fields: ${candidates.join(' | ')}`);
|
|
196
|
+
if (scored.length === 0) log('No matching sections found.');
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
const scoredHeadings = new Set(scored.map((s) => s.heading));
|
|
@@ -224,13 +225,13 @@ async function autoprAmend(options) {
|
|
|
224
225
|
const pr = await fetchActivePr(branch);
|
|
225
226
|
const prNumber = pr.pullRequestId;
|
|
226
227
|
const prUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_git/${DEVOPS_REPO}/pullrequest/${prNumber}`;
|
|
227
|
-
|
|
228
|
+
log(`Found PR #${prNumber}: ${prUrl}`);
|
|
228
229
|
|
|
229
230
|
const ids = options.trident ?? [];
|
|
230
231
|
const jiras = options.jira ?? [];
|
|
231
232
|
|
|
232
233
|
const tasks = ids.length > 0 ? await Promise.all(ids.map(fetchTask)) : [];
|
|
233
|
-
tasks.forEach((t) =>
|
|
234
|
+
tasks.forEach((t) => log(`Task: ${t.name}`));
|
|
234
235
|
|
|
235
236
|
const newLinks = [
|
|
236
237
|
...ids.map((id) => `${process.env.TRIDENT_URL}/odoo/my-tasks/${id}`),
|
|
@@ -241,20 +242,18 @@ async function autoprAmend(options) {
|
|
|
241
242
|
? `${pr.description}\n${newLinks.join('\n')}`
|
|
242
243
|
: newLinks.join('\n');
|
|
243
244
|
await patchDevopsPrDescription(prNumber, updatedDescription);
|
|
244
|
-
|
|
245
|
+
log('PR description updated');
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
for (let i = 0; i < ids.length; i++) {
|
|
248
|
-
await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl
|
|
249
|
+
await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl);
|
|
249
250
|
}
|
|
250
|
-
if (ids.length > 0)
|
|
251
|
+
if (ids.length > 0) log('Checklist updated');
|
|
251
252
|
|
|
252
253
|
const content = readFileSync(changelogPath, 'utf-8');
|
|
253
254
|
const existing = findLineByPrNumber(content, prNumber);
|
|
254
255
|
if (!existing) {
|
|
255
|
-
|
|
256
|
-
`Warning: no changelog line found for PR #${prNumber} — skipping changelog update`
|
|
257
|
-
);
|
|
256
|
+
log(`Warning: no changelog line found for PR #${prNumber} — skipping changelog update`);
|
|
258
257
|
return;
|
|
259
258
|
}
|
|
260
259
|
const lines = content.split('\n');
|
|
@@ -264,8 +263,8 @@ async function autoprAmend(options) {
|
|
|
264
263
|
await execGit(['add', 'CHANGELOG.md'], ADDONS_PATH);
|
|
265
264
|
await execGit(['commit', '-m', '[DOC][CHANGELOG] Changelog'], ADDONS_PATH);
|
|
266
265
|
await execGit(['push'], ADDONS_PATH);
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
log('Changelog updated and pushed');
|
|
267
|
+
log('\nReminder: squash the two changelog commits before merging the PR.');
|
|
269
268
|
}
|
|
270
269
|
|
|
271
270
|
async function autopr(options) {
|
|
@@ -278,7 +277,7 @@ async function autopr(options) {
|
|
|
278
277
|
const hasTridents = ids.length > 0;
|
|
279
278
|
|
|
280
279
|
const tasks = hasTridents ? await Promise.all(ids.map((id) => fetchTask(id))) : [];
|
|
281
|
-
tasks.forEach((t) =>
|
|
280
|
+
tasks.forEach((t) => log(`Task: ${t.name}`));
|
|
282
281
|
|
|
283
282
|
const content = readFileSync(changelogPath, 'utf-8');
|
|
284
283
|
|
|
@@ -286,7 +285,7 @@ async function autopr(options) {
|
|
|
286
285
|
|
|
287
286
|
let appendMode = false;
|
|
288
287
|
if (duplicate) {
|
|
289
|
-
|
|
288
|
+
log(`\nExisting entry (line ${duplicate.lineNumber + 1}):\n ${duplicate.line}`);
|
|
290
289
|
const { confirm } = await inquirer.prompt([
|
|
291
290
|
{
|
|
292
291
|
type: 'confirm',
|
|
@@ -315,19 +314,19 @@ async function autopr(options) {
|
|
|
315
314
|
}
|
|
316
315
|
|
|
317
316
|
await execGit(['checkout', '-b', branch], ADDONS_PATH);
|
|
318
|
-
|
|
317
|
+
log(`Branch created: ${branch}`);
|
|
319
318
|
await execGit(['push', '-u', 'origin', branch], ADDONS_PATH);
|
|
320
319
|
|
|
321
320
|
const prTitle = options.name ?? tasks[0]?.name ?? branch;
|
|
322
321
|
const prDescription = buildPrDescription(ids, options.jira ?? []);
|
|
323
322
|
const { id: prNumber, url: prUrl } = await createDevopsPR(branch, prTitle, prDescription);
|
|
324
|
-
|
|
323
|
+
log(`PR opened: #${prNumber} — ${prUrl}`);
|
|
325
324
|
|
|
326
325
|
if (hasTridents) {
|
|
327
326
|
for (let i = 0; i < ids.length; i++) {
|
|
328
|
-
await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl
|
|
327
|
+
await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl);
|
|
329
328
|
}
|
|
330
|
-
|
|
329
|
+
log('Checklist updated');
|
|
331
330
|
}
|
|
332
331
|
|
|
333
332
|
const lines = content.split('\n');
|
|
@@ -365,12 +364,12 @@ async function autopr(options) {
|
|
|
365
364
|
}
|
|
366
365
|
|
|
367
366
|
await fs.writeFile(changelogPath, lines.join('\n'));
|
|
368
|
-
|
|
367
|
+
log('Changelog entry written');
|
|
369
368
|
|
|
370
369
|
await execGit(['add', 'CHANGELOG.md'], ADDONS_PATH);
|
|
371
370
|
await execGit(['commit', '-m', '[DOC][CHANGELOG] Changelog'], ADDONS_PATH);
|
|
372
371
|
await execGit(['push'], ADDONS_PATH);
|
|
373
|
-
|
|
372
|
+
log('Changelog committed and pushed');
|
|
374
373
|
}
|
|
375
374
|
|
|
376
375
|
export { autopr };
|