@k0t0vich/meta-agents-template 0.1.9 → 0.1.10
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.10 - 2026-03-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Implemented GitHub TrackerGateway adapter operations instead of `not implemented` stubs (`CREATE_TASK`, `SET_STATUS`, `ASSIGN_SPRINT`, `COMMIT_BY_NAME`).
|
|
9
|
+
- Added safe `dryRun` support for GitHub tracker operations to validate flows without side effects.
|
|
10
|
+
- Added smoke regression check for `meta:ops CREATE_TASK` (dry-run) to prevent future regressions where command exists but adapter is stubbed.
|
|
11
|
+
|
|
5
12
|
## 0.1.9 - 2026-03-20
|
|
6
13
|
|
|
7
14
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,4 +1,549 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
const STATUS_LABELS = ["TODO", "IN_PROGRESS", "REVIEW", "READY", "BLOCKED", "DONE", "PUBLISH"];
|
|
5
|
+
const STATUS_COLORS = {
|
|
6
|
+
IN_PROGRESS: "FBCA04",
|
|
7
|
+
REVIEW: "5319E7",
|
|
8
|
+
READY: "0E8A16",
|
|
9
|
+
BLOCKED: "B60205",
|
|
10
|
+
DONE: "1D76DB",
|
|
11
|
+
PUBLISH: "0052CC",
|
|
12
|
+
};
|
|
13
|
+
STATUS_COLORS.TODO = "D4C5F9";
|
|
14
|
+
const FALLBACK_COLORS = {
|
|
15
|
+
type: "C2E0C6",
|
|
16
|
+
owner: "0E8A16",
|
|
17
|
+
tracker: "0052CC",
|
|
18
|
+
sprint: "F9D0C4",
|
|
19
|
+
default: "BFDADC",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function run(command, args, allowFailure = false) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync(command, args, {
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
}).trim();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (allowFailure) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
const stderr = String(error?.stderr || "").trim();
|
|
34
|
+
const stdout = String(error?.stdout || "").trim();
|
|
35
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${stderr || stdout || error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJson(command, args, allowFailure = false) {
|
|
40
|
+
const raw = run(command, args, allowFailure);
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`failed to parse JSON from '${command} ${args.join(" ")}': ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureGhReady() {
|
|
52
|
+
run("gh", ["--version"], false);
|
|
53
|
+
const auth = run("gh", ["auth", "status"], true);
|
|
54
|
+
if (!auth) {
|
|
55
|
+
throw new Error("gh is not authenticated. Run 'gh auth login' first.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toRepoArgs(repo) {
|
|
60
|
+
if (!repo) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return ["--repo", String(repo)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toNonEmptyString(value) {
|
|
67
|
+
if (value === null || value === undefined) {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
return String(value).trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pickFirstNonEmpty(...values) {
|
|
74
|
+
for (const value of values) {
|
|
75
|
+
const normalized = toNonEmptyString(value);
|
|
76
|
+
if (normalized) {
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseIssueNumber(value) {
|
|
84
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const source = toNonEmptyString(value);
|
|
89
|
+
if (!source) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const direct = source.match(/^#?(\d+)$/);
|
|
94
|
+
if (direct) {
|
|
95
|
+
return direct[1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const issueToken = source.match(/(?:^|[\/\s])issue-(\d+)(?:-|$|\s)/i);
|
|
99
|
+
if (issueToken) {
|
|
100
|
+
return issueToken[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const githubUrl = source.match(/\/issues\/(\d+)(?:$|[/?#])/i);
|
|
104
|
+
if (githubUrl) {
|
|
105
|
+
return githubUrl[1];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ref = source.match(/#(\d+)/);
|
|
109
|
+
if (ref) {
|
|
110
|
+
return ref[1];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeStatus(value, fallback = "TODO") {
|
|
117
|
+
const normalized = toNonEmptyString(value).toUpperCase() || fallback;
|
|
118
|
+
if (!STATUS_LABELS.includes(normalized)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`unsupported status '${normalized}'. Allowed: ${STATUS_LABELS.join(", ")}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeSprintLabel(value) {
|
|
127
|
+
const sprint = toNonEmptyString(value);
|
|
128
|
+
if (!sprint) {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
if (/^sprint:/i.test(sprint)) {
|
|
132
|
+
return sprint.replace(/^sprint:/i, "sprint:");
|
|
133
|
+
}
|
|
134
|
+
return `sprint:${sprint}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeOwnerLabel(value) {
|
|
138
|
+
const owner = toNonEmptyString(value);
|
|
139
|
+
if (!owner) {
|
|
140
|
+
return "owner:Engineering Agent";
|
|
141
|
+
}
|
|
142
|
+
if (/^owner:/i.test(owner)) {
|
|
143
|
+
return owner.replace(/^owner:/i, "owner:");
|
|
144
|
+
}
|
|
145
|
+
return `owner:${owner}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeTypeLabel(value) {
|
|
149
|
+
const kind = toNonEmptyString(value).toLowerCase() || "task";
|
|
150
|
+
if (kind.startsWith("type:")) {
|
|
151
|
+
return kind;
|
|
152
|
+
}
|
|
153
|
+
return `type:${kind}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseLabelsInput(value) {
|
|
157
|
+
if (!value) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.map((item) => toNonEmptyString(item)).filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return String(value)
|
|
166
|
+
.split(",")
|
|
167
|
+
.map((item) => item.trim())
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function labelColor(name) {
|
|
172
|
+
if (STATUS_COLORS[name]) {
|
|
173
|
+
return STATUS_COLORS[name];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lower = toNonEmptyString(name).toLowerCase();
|
|
177
|
+
if (lower.startsWith("type:")) {
|
|
178
|
+
return FALLBACK_COLORS.type;
|
|
179
|
+
}
|
|
180
|
+
if (lower.startsWith("owner:")) {
|
|
181
|
+
return FALLBACK_COLORS.owner;
|
|
182
|
+
}
|
|
183
|
+
if (lower.startsWith("tracker:")) {
|
|
184
|
+
return FALLBACK_COLORS.tracker;
|
|
185
|
+
}
|
|
186
|
+
if (lower.startsWith("sprint:")) {
|
|
187
|
+
return FALLBACK_COLORS.sprint;
|
|
188
|
+
}
|
|
189
|
+
return FALLBACK_COLORS.default;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mergeUnique(values) {
|
|
193
|
+
return Array.from(new Set(values.map((item) => toNonEmptyString(item)).filter(Boolean)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildCreateTaskLabels(payload) {
|
|
197
|
+
const status = normalizeStatus(payload.status || "TODO");
|
|
198
|
+
const labels = [
|
|
199
|
+
...parseLabelsInput(payload.labels),
|
|
200
|
+
status,
|
|
201
|
+
normalizeTypeLabel(payload.type),
|
|
202
|
+
normalizeOwnerLabel(payload.owner || payload.ownerRole),
|
|
203
|
+
"tracker:github",
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const sprintLabel = normalizeSprintLabel(payload.sprint);
|
|
207
|
+
if (sprintLabel) {
|
|
208
|
+
labels.push(sprintLabel);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return mergeUnique(labels);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderBulletedValue(value, fallbackText) {
|
|
215
|
+
const text = toNonEmptyString(value);
|
|
216
|
+
if (!text) {
|
|
217
|
+
return `- ${fallbackText}`;
|
|
218
|
+
}
|
|
219
|
+
if (text.includes("\n")) {
|
|
220
|
+
const lines = text
|
|
221
|
+
.split("\n")
|
|
222
|
+
.map((line) => line.trim())
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
if (lines.length === 0) {
|
|
225
|
+
return `- ${fallbackText}`;
|
|
226
|
+
}
|
|
227
|
+
return lines.map((line) => `- ${line}`).join("\n");
|
|
228
|
+
}
|
|
229
|
+
return `- ${text}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderCreateTaskBody(payload, labels) {
|
|
233
|
+
const description = renderBulletedValue(
|
|
234
|
+
payload.description,
|
|
235
|
+
"заполнить описание задачи",
|
|
236
|
+
);
|
|
237
|
+
const verifiability = renderBulletedValue(
|
|
238
|
+
payload.verifiability || payload.verification,
|
|
239
|
+
"strict: TBD; statistical: TBD; human: TBD",
|
|
240
|
+
);
|
|
241
|
+
const doneBlock = renderBulletedValue(
|
|
242
|
+
payload.done,
|
|
243
|
+
"[ ] Не начато",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const metadata = [];
|
|
247
|
+
metadata.push(`- tracker: github`);
|
|
248
|
+
metadata.push(`- labels: ${labels.join(", ")}`);
|
|
249
|
+
const sprintLabel = normalizeSprintLabel(payload.sprint);
|
|
250
|
+
if (sprintLabel) {
|
|
251
|
+
metadata.push(`- sprint: ${sprintLabel.replace(/^sprint:/i, "")}`);
|
|
252
|
+
}
|
|
253
|
+
const epicRef = parseIssueNumber(payload.epic || payload.linkToEpic || payload.parent);
|
|
254
|
+
if (epicRef) {
|
|
255
|
+
metadata.push(`- epic: #${epicRef}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [
|
|
259
|
+
"## PRD Step",
|
|
260
|
+
"",
|
|
261
|
+
"### Описание",
|
|
262
|
+
description,
|
|
263
|
+
"",
|
|
264
|
+
"### Проверяемость",
|
|
265
|
+
verifiability,
|
|
266
|
+
"",
|
|
267
|
+
"### Что сделано",
|
|
268
|
+
doneBlock,
|
|
269
|
+
"",
|
|
270
|
+
"## Metadata",
|
|
271
|
+
...metadata,
|
|
272
|
+
].join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ensureLabelExists(name, repo) {
|
|
276
|
+
const repoArgs = toRepoArgs(repo);
|
|
277
|
+
const exists = readJson(
|
|
278
|
+
"gh",
|
|
279
|
+
["label", "list", ...repoArgs, "--search", name, "--limit", "100", "--json", "name"],
|
|
280
|
+
true,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (Array.isArray(exists) && exists.some((item) => item?.name === name)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const color = labelColor(name);
|
|
288
|
+
const description = "Auto-created by meta:ops github adapter";
|
|
289
|
+
const created = run(
|
|
290
|
+
"gh",
|
|
291
|
+
["label", "create", name, "--color", color, "--description", description, ...repoArgs],
|
|
292
|
+
true,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (!created) {
|
|
296
|
+
const existsAfter = readJson(
|
|
297
|
+
"gh",
|
|
298
|
+
["label", "list", ...repoArgs, "--search", name, "--limit", "100", "--json", "name"],
|
|
299
|
+
true,
|
|
300
|
+
);
|
|
301
|
+
if (!(Array.isArray(existsAfter) && existsAfter.some((item) => item?.name === name))) {
|
|
302
|
+
throw new Error(`failed to create label '${name}'`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function ensureLabelsExist(labels, repo) {
|
|
308
|
+
for (const label of labels) {
|
|
309
|
+
ensureLabelExists(label, repo);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveTaskIssue(payload) {
|
|
314
|
+
const issue = parseIssueNumber(payload.task || payload.issue || payload.taskId || payload.id);
|
|
315
|
+
if (!issue) {
|
|
316
|
+
throw new Error("missing issue reference in payload (task/issue/taskId/id)");
|
|
317
|
+
}
|
|
318
|
+
return issue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getIssueLabels(issue, repo) {
|
|
322
|
+
const repoArgs = toRepoArgs(repo);
|
|
323
|
+
const issueData = readJson(
|
|
324
|
+
"gh",
|
|
325
|
+
["issue", "view", String(issue), "--json", "labels", ...repoArgs],
|
|
326
|
+
false,
|
|
327
|
+
);
|
|
328
|
+
const labels = issueData?.labels || [];
|
|
329
|
+
return labels.map((item) => toNonEmptyString(item?.name)).filter(Boolean);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function removeAndAddIssueLabels(issue, repo, labelsToRemove, labelsToAdd) {
|
|
333
|
+
const repoArgs = toRepoArgs(repo);
|
|
334
|
+
const args = ["issue", "edit", String(issue), ...repoArgs];
|
|
335
|
+
|
|
336
|
+
for (const label of labelsToRemove) {
|
|
337
|
+
args.push("--remove-label", label);
|
|
338
|
+
}
|
|
339
|
+
for (const label of labelsToAdd) {
|
|
340
|
+
args.push("--add-label", label);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
run("gh", args, false);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function commentIssue(issue, repo, body) {
|
|
347
|
+
const content = toNonEmptyString(body);
|
|
348
|
+
if (!content) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const repoArgs = toRepoArgs(repo);
|
|
352
|
+
run("gh", ["issue", "comment", String(issue), "--body", content, ...repoArgs], false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function gitCurrentBranch() {
|
|
356
|
+
return run("git", ["rev-parse", "--abbrev-ref", "HEAD"], true);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function gitAdd(files) {
|
|
360
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
361
|
+
run("git", ["add", ...files.map((item) => String(item))], false);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
run("git", ["add", "-A"], false);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function ensureStagedChanges() {
|
|
368
|
+
const staged = run("git", ["diff", "--cached", "--name-only"], true);
|
|
369
|
+
if (!staged) {
|
|
370
|
+
throw new Error("no staged changes to commit");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function buildCommitMessage(payload) {
|
|
375
|
+
const explicit = pickFirstNonEmpty(payload.message, payload.commitMessage);
|
|
376
|
+
if (explicit) {
|
|
377
|
+
return explicit;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const issue = parseIssueNumber(payload.task || payload.issue || payload.taskId || payload.id);
|
|
381
|
+
if (!issue) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
"missing commit message. Provide payload.message or payload.issue/task to build '#<issue> <summary>'",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const summary = pickFirstNonEmpty(payload.shortName, payload.title, payload.name, "update");
|
|
388
|
+
return `#${issue} ${summary}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function createTask(payload = {}) {
|
|
392
|
+
const title = pickFirstNonEmpty(payload.shortName, payload.title, payload.name);
|
|
393
|
+
if (!title) {
|
|
394
|
+
throw new Error("missing task title (shortName/title/name)");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const repo = pickFirstNonEmpty(payload.repo);
|
|
398
|
+
const labels = buildCreateTaskLabels(payload);
|
|
399
|
+
const body = renderCreateTaskBody(payload, labels);
|
|
400
|
+
|
|
401
|
+
if (payload.dryRun) {
|
|
402
|
+
console.log(
|
|
403
|
+
JSON.stringify(
|
|
404
|
+
{
|
|
405
|
+
command: "CREATE_TASK",
|
|
406
|
+
mode: "dry-run",
|
|
407
|
+
repo: repo || "current",
|
|
408
|
+
title,
|
|
409
|
+
labels,
|
|
410
|
+
},
|
|
411
|
+
null,
|
|
412
|
+
2,
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
ensureGhReady();
|
|
419
|
+
ensureLabelsExist(labels, repo);
|
|
420
|
+
|
|
421
|
+
const repoArgs = toRepoArgs(repo);
|
|
422
|
+
const args = ["issue", "create", "--title", title, "--body", body, ...repoArgs];
|
|
423
|
+
for (const label of labels) {
|
|
424
|
+
args.push("--label", label);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const issueUrl = run("gh", args, false);
|
|
428
|
+
const issue = parseIssueNumber(issueUrl);
|
|
429
|
+
const epic = parseIssueNumber(payload.epic || payload.linkToEpic || payload.parent);
|
|
430
|
+
if (issue && epic) {
|
|
431
|
+
commentIssue(issue, repo, `Linked to epic #${epic}.`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log(`github CREATE_TASK: ${issueUrl}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function setStatus(payload = {}) {
|
|
438
|
+
const issue = resolveTaskIssue(payload);
|
|
439
|
+
const repo = pickFirstNonEmpty(payload.repo);
|
|
440
|
+
const targetStatus = normalizeStatus(payload.status);
|
|
441
|
+
|
|
442
|
+
if (payload.dryRun) {
|
|
443
|
+
console.log(
|
|
444
|
+
JSON.stringify(
|
|
445
|
+
{
|
|
446
|
+
command: "SET_STATUS",
|
|
447
|
+
mode: "dry-run",
|
|
448
|
+
repo: repo || "current",
|
|
449
|
+
issue,
|
|
450
|
+
status: targetStatus,
|
|
451
|
+
},
|
|
452
|
+
null,
|
|
453
|
+
2,
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
ensureGhReady();
|
|
460
|
+
ensureLabelsExist([targetStatus], repo);
|
|
461
|
+
|
|
462
|
+
const currentLabels = getIssueLabels(issue, repo);
|
|
463
|
+
const removeStatuses = currentLabels.filter(
|
|
464
|
+
(label) => STATUS_LABELS.includes(label) && label !== targetStatus,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
removeAndAddIssueLabels(issue, repo, removeStatuses, [targetStatus]);
|
|
468
|
+
const reason = pickFirstNonEmpty(payload.reason);
|
|
469
|
+
if (reason) {
|
|
470
|
+
commentIssue(issue, repo, `Status -> ${targetStatus}. Reason: ${reason}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
console.log(`github SET_STATUS: #${issue} -> ${targetStatus}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function commitByName(payload = {}) {
|
|
477
|
+
const branch = gitCurrentBranch();
|
|
478
|
+
const expectedBranch = pickFirstNonEmpty(payload.branch);
|
|
479
|
+
if (expectedBranch && branch && branch !== expectedBranch) {
|
|
480
|
+
throw new Error(`branch mismatch: current '${branch}', expected '${expectedBranch}'`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const message = buildCommitMessage(payload);
|
|
484
|
+
const files = Array.isArray(payload.files) ? payload.files : [];
|
|
485
|
+
|
|
486
|
+
if (payload.dryRun) {
|
|
487
|
+
console.log(
|
|
488
|
+
JSON.stringify(
|
|
489
|
+
{
|
|
490
|
+
command: "COMMIT_BY_NAME",
|
|
491
|
+
mode: "dry-run",
|
|
492
|
+
branch: branch || "unknown",
|
|
493
|
+
message,
|
|
494
|
+
files: files.length > 0 ? files : ["-A"],
|
|
495
|
+
push: Boolean(payload.push),
|
|
496
|
+
},
|
|
497
|
+
null,
|
|
498
|
+
2,
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
gitAdd(files);
|
|
505
|
+
ensureStagedChanges();
|
|
506
|
+
run("git", ["commit", "-m", message], false);
|
|
507
|
+
if (payload.push) {
|
|
508
|
+
run("git", ["push"], false);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log(`github COMMIT_BY_NAME: ${message}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function assignSprint(payload = {}) {
|
|
515
|
+
const issue = resolveTaskIssue(payload);
|
|
516
|
+
const repo = pickFirstNonEmpty(payload.repo);
|
|
517
|
+
const sprintLabel = normalizeSprintLabel(payload.sprint);
|
|
518
|
+
if (!sprintLabel) {
|
|
519
|
+
throw new Error("missing sprint in payload");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (payload.dryRun) {
|
|
523
|
+
console.log(
|
|
524
|
+
JSON.stringify(
|
|
525
|
+
{
|
|
526
|
+
command: "ASSIGN_SPRINT",
|
|
527
|
+
mode: "dry-run",
|
|
528
|
+
repo: repo || "current",
|
|
529
|
+
issue,
|
|
530
|
+
sprint: sprintLabel,
|
|
531
|
+
},
|
|
532
|
+
null,
|
|
533
|
+
2,
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
ensureGhReady();
|
|
540
|
+
ensureLabelsExist([sprintLabel], repo);
|
|
541
|
+
|
|
542
|
+
const currentLabels = getIssueLabels(issue, repo);
|
|
543
|
+
const removeSprints = currentLabels.filter(
|
|
544
|
+
(label) => label.toLowerCase().startsWith("sprint:") && label !== sprintLabel,
|
|
545
|
+
);
|
|
546
|
+
removeAndAddIssueLabels(issue, repo, removeSprints, [sprintLabel]);
|
|
547
|
+
|
|
548
|
+
console.log(`github ASSIGN_SPRINT: #${issue} -> ${sprintLabel}`);
|
|
549
|
+
}
|