@starterculture/devkeep-actions 0.2.0 → 0.3.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/dist/draft-log-entry.js
CHANGED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../modules/core/dist/github/client.js
|
|
4
|
+
import { Octokit } from "@octokit/rest";
|
|
5
|
+
function createGithubClient(token) {
|
|
6
|
+
if (!token) {
|
|
7
|
+
throw new Error("GitHub token is required");
|
|
8
|
+
}
|
|
9
|
+
return new Octokit({ auth: token });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ../../modules/core/dist/github/retryOnConflict.js
|
|
13
|
+
async function retryOnConflict(attempt, maxAttempts = 3) {
|
|
14
|
+
for (let tryNumber = 1; ; tryNumber++) {
|
|
15
|
+
try {
|
|
16
|
+
return await attempt();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const status = error.status;
|
|
19
|
+
if (tryNumber >= maxAttempts || status !== 409 && status !== 422) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 300 * tryNumber));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ../../modules/core/dist/tokenUsage.js
|
|
28
|
+
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
29
|
+
function addTokenUsage(a, b) {
|
|
30
|
+
return { inputTokens: a.inputTokens + b.inputTokens, outputTokens: a.outputTokens + b.outputTokens };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ../../modules/core/dist/rollover.js
|
|
34
|
+
var ZERO_ROLLOVER_USAGE = {
|
|
35
|
+
today: { key: "", usage: ZERO_USAGE },
|
|
36
|
+
month: { key: "", usage: ZERO_USAGE }
|
|
37
|
+
};
|
|
38
|
+
function todayKey(now = /* @__PURE__ */ new Date()) {
|
|
39
|
+
const year = now.getFullYear();
|
|
40
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
41
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
42
|
+
return `${year}-${month}-${day}`;
|
|
43
|
+
}
|
|
44
|
+
function monthKey(now = /* @__PURE__ */ new Date()) {
|
|
45
|
+
return todayKey(now).slice(0, 7);
|
|
46
|
+
}
|
|
47
|
+
function addRolloverUsage(current, delta, now = /* @__PURE__ */ new Date()) {
|
|
48
|
+
const nowTodayKey = todayKey(now);
|
|
49
|
+
const nowMonthKey = monthKey(now);
|
|
50
|
+
return {
|
|
51
|
+
today: {
|
|
52
|
+
key: nowTodayKey,
|
|
53
|
+
usage: addTokenUsage(current.today.key === nowTodayKey ? current.today.usage : ZERO_USAGE, delta)
|
|
54
|
+
},
|
|
55
|
+
month: {
|
|
56
|
+
key: nowMonthKey,
|
|
57
|
+
usage: addTokenUsage(current.month.key === nowMonthKey ? current.month.usage : ZERO_USAGE, delta)
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ../../modules/core/dist/claude/client.js
|
|
63
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
64
|
+
function createClaudeClient(apiKey) {
|
|
65
|
+
const key = apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
66
|
+
if (!key) {
|
|
67
|
+
throw new Error("ANTHROPIC_API_KEY is not set");
|
|
68
|
+
}
|
|
69
|
+
return new Anthropic({ apiKey: key });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ../../modules/core/dist/claude/complete.js
|
|
73
|
+
var DEFAULT_MAX_CONTINUATIONS = 5;
|
|
74
|
+
var CONTINUATION_USER_MESSAGE = "Continue exactly where you left off. Do not repeat any text already written, and do not add any preamble or commentary \u2014 resume the previous response verbatim from the exact cutoff point.";
|
|
75
|
+
async function createCompleteMessage(claude, params, { maxContinuations = DEFAULT_MAX_CONTINUATIONS, timeoutMs, signal } = {}) {
|
|
76
|
+
let fullText = "";
|
|
77
|
+
let usage = ZERO_USAGE;
|
|
78
|
+
const requestOptions = timeoutMs !== void 0 || signal !== void 0 ? { timeout: timeoutMs, signal } : void 0;
|
|
79
|
+
for (let attempt = 0; attempt <= maxContinuations; attempt++) {
|
|
80
|
+
const messages = fullText ? [
|
|
81
|
+
...params.messages,
|
|
82
|
+
{ role: "assistant", content: fullText },
|
|
83
|
+
{ role: "user", content: CONTINUATION_USER_MESSAGE }
|
|
84
|
+
] : params.messages;
|
|
85
|
+
const message = requestOptions ? await claude.messages.create({ ...params, messages }, requestOptions) : await claude.messages.create({ ...params, messages });
|
|
86
|
+
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
87
|
+
fullText += text;
|
|
88
|
+
usage = addTokenUsage(usage, {
|
|
89
|
+
inputTokens: message.usage?.input_tokens ?? 0,
|
|
90
|
+
outputTokens: message.usage?.output_tokens ?? 0
|
|
91
|
+
});
|
|
92
|
+
if (message.stop_reason !== "max_tokens") {
|
|
93
|
+
return { text: fullText, usage };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Claude response still hit max_tokens after ${maxContinuations} continuation attempts \u2014 refusing to return truncated content.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ../../modules/core/dist/modelRegistry.js
|
|
100
|
+
var MODEL_REGISTRY = {
|
|
101
|
+
fast: {
|
|
102
|
+
description: "Quick, cheap work where depth matters least.",
|
|
103
|
+
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5" }]
|
|
104
|
+
},
|
|
105
|
+
balanced: {
|
|
106
|
+
description: "The everyday tier: chat, log entries, release notes, the daily digest. Frequent enough that cost matters.",
|
|
107
|
+
models: [{ id: "claude-sonnet-5", label: "Sonnet 5" }]
|
|
108
|
+
},
|
|
109
|
+
capable: {
|
|
110
|
+
description: "Deeper reasoning for infrequent, high-value work where getting it right outweighs the cost.",
|
|
111
|
+
models: [{ id: "claude-opus-5", label: "Opus 5" }]
|
|
112
|
+
},
|
|
113
|
+
max: {
|
|
114
|
+
description: "Maximum depth, for long-running agentic work. Opt-in \u2014 the most expensive tier.",
|
|
115
|
+
models: [{ id: "claude-fable-5", label: "Fable 5" }]
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function defaultModelFor(tier) {
|
|
119
|
+
const [current] = MODEL_REGISTRY[tier].models;
|
|
120
|
+
if (!current) {
|
|
121
|
+
throw new Error(`Model tier "${tier}" has no models registered`);
|
|
122
|
+
}
|
|
123
|
+
return current.id;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ../../modules/devlore/dist/prompt.js
|
|
127
|
+
function buildReleaseNotesPrompt(input) {
|
|
128
|
+
return [
|
|
129
|
+
`Project: ${input.projectName}`,
|
|
130
|
+
`Release: ${input.tag}`,
|
|
131
|
+
input.previousTag ? `Previous release: ${input.previousTag}. The entries below are the work done since then.` : "This is the first release, so the entries below cover everything so far.",
|
|
132
|
+
"",
|
|
133
|
+
"Below are the decision log entries Devkeep recorded for the work in this release.",
|
|
134
|
+
"",
|
|
135
|
+
...input.logEntries,
|
|
136
|
+
"",
|
|
137
|
+
"Write release notes for the people who use this product. Lead with what they can now do that they could not before, and what changed in behaviour they will notice. Group related changes rather than listing entries one by one.",
|
|
138
|
+
"",
|
|
139
|
+
"Plain language, not commit-history jargon: no file paths, no function names, no internal module names unless the user genuinely interacts with them by name.",
|
|
140
|
+
"",
|
|
141
|
+
'If this release contains nothing user-facing \u2014 internal refactoring, tooling, workflow changes \u2014 say exactly that in a sentence or two. Do not manufacture user-facing significance that is not there; an honest "this release contains internal changes only" is the correct output, and inventing a feature to fill the page is worse than a short note.',
|
|
142
|
+
"",
|
|
143
|
+
"Do NOT include a top-level heading \u2014 one is added automatically. Start directly with the content.",
|
|
144
|
+
"",
|
|
145
|
+
"Return the notes only \u2014 no preamble, no meta-commentary."
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ../../modules/devlore/dist/redact.js
|
|
150
|
+
var REDACTED = "[REDACTED]";
|
|
151
|
+
var KNOWN_SECRET_PATTERNS = [
|
|
152
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
153
|
+
/gh[pousr]_[A-Za-z0-9]{36,}/g,
|
|
154
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
155
|
+
/sk-ant-[A-Za-z0-9_-]{20,}/g,
|
|
156
|
+
/sk-[A-Za-z0-9]{20,}/g,
|
|
157
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
158
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
159
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g
|
|
160
|
+
];
|
|
161
|
+
var SECRET_KEYWORD = "(?:API[_-]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CREDENTIAL)";
|
|
162
|
+
var ASSIGNMENT_PATTERN = new RegExp(`((?:[A-Za-z0-9]+[_-])*${SECRET_KEYWORD}(?:[_-][A-Za-z0-9]+)*\\s*[:=]\\s*)(["']?)([^"'\`\\s]+)\\2`, "gi");
|
|
163
|
+
function redactText(text) {
|
|
164
|
+
let result = text;
|
|
165
|
+
for (const pattern of KNOWN_SECRET_PATTERNS) {
|
|
166
|
+
result = result.replace(pattern, REDACTED);
|
|
167
|
+
}
|
|
168
|
+
result = result.replace(ASSIGNMENT_PATTERN, (_match, prefix, quote) => `${prefix}${quote}${REDACTED}${quote}`);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ../../modules/devlore/dist/ciUsage.js
|
|
173
|
+
var EMPTY_PROJECT_USAGE = {
|
|
174
|
+
draftLogEntry: ZERO_USAGE,
|
|
175
|
+
consolidateRelease: ZERO_USAGE,
|
|
176
|
+
syncUserManual: ZERO_USAGE,
|
|
177
|
+
syncTestPlan: ZERO_USAGE,
|
|
178
|
+
syncVisualizer: ZERO_USAGE,
|
|
179
|
+
analyzeCodebase: ZERO_USAGE,
|
|
180
|
+
syncOnboarding: ZERO_USAGE,
|
|
181
|
+
releaseNotes: ZERO_USAGE,
|
|
182
|
+
rollover: ZERO_ROLLOVER_USAGE
|
|
183
|
+
};
|
|
184
|
+
function usagePath(projectName) {
|
|
185
|
+
return `projects/${projectName}/usage.json`;
|
|
186
|
+
}
|
|
187
|
+
async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
188
|
+
try {
|
|
189
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
190
|
+
owner,
|
|
191
|
+
repo,
|
|
192
|
+
path: usagePath(projectName)
|
|
193
|
+
});
|
|
194
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
195
|
+
return { usage: EMPTY_PROJECT_USAGE };
|
|
196
|
+
}
|
|
197
|
+
const parsed = JSON.parse(Buffer.from(data.content, "base64").toString("utf-8"));
|
|
198
|
+
const usage = { ...EMPTY_PROJECT_USAGE, ...parsed };
|
|
199
|
+
return { usage, sha: data.sha };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (isNotFound(error)) {
|
|
202
|
+
return { usage: EMPTY_PROJECT_USAGE };
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
208
|
+
return {
|
|
209
|
+
...current,
|
|
210
|
+
[source]: addTokenUsage(current[source], delta),
|
|
211
|
+
rollover: addRolloverUsage(current.rollover, delta, now)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
215
|
+
const { owner, repo, projectName, branch } = params;
|
|
216
|
+
await retryOnConflict(async () => {
|
|
217
|
+
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
218
|
+
const updated = addUsage(current, source, delta, now);
|
|
219
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
220
|
+
owner,
|
|
221
|
+
repo,
|
|
222
|
+
path: usagePath(projectName),
|
|
223
|
+
message: `Record ${source} token usage: ${projectName}`,
|
|
224
|
+
content: Buffer.from(JSON.stringify(updated, null, 2), "utf-8").toString("base64"),
|
|
225
|
+
branch,
|
|
226
|
+
sha
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function isNotFound(error) {
|
|
231
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ../../modules/devlore/dist/logEntries.js
|
|
235
|
+
function logEntriesDir(projectName) {
|
|
236
|
+
return `projects/${projectName}/log`;
|
|
237
|
+
}
|
|
238
|
+
async function listLogEntries(octokit, { owner, repo, projectName }) {
|
|
239
|
+
const dir = logEntriesDir(projectName);
|
|
240
|
+
let files;
|
|
241
|
+
try {
|
|
242
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dir });
|
|
243
|
+
if (!Array.isArray(data)) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
files = data;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (isNotFound2(error)) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
const entries = await Promise.all(files.filter((file) => file.type === "file").map(async (file) => {
|
|
254
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: file.path });
|
|
255
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return { path: file.path, content: Buffer.from(data.content, "base64").toString("utf-8") };
|
|
259
|
+
}));
|
|
260
|
+
return entries.filter((entry) => entry !== null).sort((a, b) => a.path.localeCompare(b.path));
|
|
261
|
+
}
|
|
262
|
+
function isNotFound2(error) {
|
|
263
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ../../modules/devlore/dist/releaseScope.js
|
|
267
|
+
var TAG_PATTERN = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
268
|
+
function parseTag(tag) {
|
|
269
|
+
const match = tag.match(TAG_PATTERN);
|
|
270
|
+
if (!match) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
274
|
+
}
|
|
275
|
+
function compareTags(a, b) {
|
|
276
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
277
|
+
}
|
|
278
|
+
async function findPreviousTag(octokit, { owner, repo, currentTag }) {
|
|
279
|
+
const current = parseTag(currentTag);
|
|
280
|
+
if (!current) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const tags = await octokit.paginate(octokit.rest.repos.listTags, { owner, repo, per_page: 100 });
|
|
284
|
+
let best = null;
|
|
285
|
+
for (const tag of tags) {
|
|
286
|
+
const version = parseTag(tag.name);
|
|
287
|
+
if (!version || compareTags(version, current) >= 0) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!best || compareTags(version, best.version) > 0) {
|
|
291
|
+
best = { name: tag.name, version };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return best?.name ?? null;
|
|
295
|
+
}
|
|
296
|
+
async function listCommitShasSince(octokit, { owner, repo, previousTag, currentTag }) {
|
|
297
|
+
try {
|
|
298
|
+
const { data } = await octokit.rest.repos.compareCommitsWithBasehead({
|
|
299
|
+
owner,
|
|
300
|
+
repo,
|
|
301
|
+
basehead: `${previousTag}...${currentTag}`
|
|
302
|
+
});
|
|
303
|
+
return new Set(data.commits.map((commit) => commit.sha));
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function logEntryCommitSha(entry) {
|
|
309
|
+
const filename = entry.path.split("/").pop() ?? "";
|
|
310
|
+
return filename.replace(/\.md$/, "");
|
|
311
|
+
}
|
|
312
|
+
async function selectLogEntriesSincePreviousRelease({ vaultOctokit, projectOctokit, vaultOwner, vaultRepoName, projectName, projectOwner, projectRepoName, tag }) {
|
|
313
|
+
const allEntries = await listLogEntries(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName });
|
|
314
|
+
const previousTag = await findPreviousTag(projectOctokit, {
|
|
315
|
+
owner: projectOwner,
|
|
316
|
+
repo: projectRepoName,
|
|
317
|
+
currentTag: tag
|
|
318
|
+
});
|
|
319
|
+
if (!previousTag) {
|
|
320
|
+
return { logEntries: allEntries, previousTag: null };
|
|
321
|
+
}
|
|
322
|
+
const commitShas = await listCommitShasSince(projectOctokit, {
|
|
323
|
+
owner: projectOwner,
|
|
324
|
+
repo: projectRepoName,
|
|
325
|
+
previousTag,
|
|
326
|
+
currentTag: tag
|
|
327
|
+
});
|
|
328
|
+
if (!commitShas) {
|
|
329
|
+
return { logEntries: allEntries, previousTag };
|
|
330
|
+
}
|
|
331
|
+
return { logEntries: allEntries.filter((entry) => commitShas.has(logEntryCommitSha(entry))), previousTag };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ../../modules/devlore/dist/releaseNotes.js
|
|
335
|
+
function releaseNotePath(projectName, tag) {
|
|
336
|
+
return `${releaseNotesDir(projectName)}/${tag}.md`;
|
|
337
|
+
}
|
|
338
|
+
function releaseNotesHeading(projectName, tag) {
|
|
339
|
+
return `# ${projectName} \u2014 ${tag}`;
|
|
340
|
+
}
|
|
341
|
+
function releaseNotesBanner() {
|
|
342
|
+
return "> **Do not move, rename, or edit this file.** Devkeep drafted these release notes when this tag was cut.";
|
|
343
|
+
}
|
|
344
|
+
function releaseNotesDir(projectName) {
|
|
345
|
+
return `projects/${projectName}/releases`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ../../modules/devlore/dist/actions/draftReleaseNotes.js
|
|
349
|
+
var MODEL_TIER = "balanced";
|
|
350
|
+
var MAX_TOKENS = 2e4;
|
|
351
|
+
var MAX_CONTINUATIONS = 8;
|
|
352
|
+
var VAULT_BRANCH = "main";
|
|
353
|
+
async function draftReleaseNotes({ projectOctokit, vaultOctokit, claude, projectOwner, projectRepoName, vaultOwner, vaultRepoName, projectName, tag }) {
|
|
354
|
+
const { logEntries, previousTag } = await selectLogEntriesSincePreviousRelease({
|
|
355
|
+
vaultOctokit,
|
|
356
|
+
projectOctokit,
|
|
357
|
+
vaultOwner,
|
|
358
|
+
vaultRepoName,
|
|
359
|
+
projectName,
|
|
360
|
+
projectOwner,
|
|
361
|
+
projectRepoName,
|
|
362
|
+
tag
|
|
363
|
+
});
|
|
364
|
+
const prompt = buildReleaseNotesPrompt({
|
|
365
|
+
projectName,
|
|
366
|
+
tag,
|
|
367
|
+
previousTag,
|
|
368
|
+
logEntries: logEntries.map((entry) => redactText(entry.content))
|
|
369
|
+
});
|
|
370
|
+
const { text, usage } = await createCompleteMessage(claude, { model: defaultModelFor(MODEL_TIER), max_tokens: MAX_TOKENS, messages: [{ role: "user", content: prompt }] }, { maxContinuations: MAX_CONTINUATIONS });
|
|
371
|
+
const content = `${releaseNotesBanner()}
|
|
372
|
+
|
|
373
|
+
${releaseNotesHeading(projectName, tag)}
|
|
374
|
+
|
|
375
|
+
${text.trim()}
|
|
376
|
+
`;
|
|
377
|
+
const path = releaseNotePath(projectName, tag);
|
|
378
|
+
await vaultOctokit.rest.repos.createOrUpdateFileContents({
|
|
379
|
+
owner: vaultOwner,
|
|
380
|
+
repo: vaultRepoName,
|
|
381
|
+
path,
|
|
382
|
+
message: `Draft release notes: ${projectName} ${tag}`,
|
|
383
|
+
content: Buffer.from(content, "utf-8").toString("base64"),
|
|
384
|
+
branch: VAULT_BRANCH,
|
|
385
|
+
sha: await getExistingSha(vaultOctokit, vaultOwner, vaultRepoName, path)
|
|
386
|
+
});
|
|
387
|
+
if (usage.inputTokens > 0 || usage.outputTokens > 0) {
|
|
388
|
+
await recordUsage(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName, branch: VAULT_BRANCH }, "releaseNotes", usage);
|
|
389
|
+
}
|
|
390
|
+
return { path, previousTag, entryCount: logEntries.length, content, usage };
|
|
391
|
+
}
|
|
392
|
+
async function getExistingSha(octokit, owner, repo, path) {
|
|
393
|
+
try {
|
|
394
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path });
|
|
395
|
+
return Array.isArray(data) || data.type !== "file" ? void 0 : data.sha;
|
|
396
|
+
} catch {
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ../../modules/devlore/dist/bin/draftReleaseNotesCli.js
|
|
402
|
+
function requireEnv(env, name) {
|
|
403
|
+
const value = env[name];
|
|
404
|
+
if (!value) {
|
|
405
|
+
throw new Error(`${name} is not set`);
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
var defaultDeps = {
|
|
410
|
+
env: process.env,
|
|
411
|
+
draftReleaseNotes,
|
|
412
|
+
createGithubClient,
|
|
413
|
+
createClaudeClient,
|
|
414
|
+
log: console.log
|
|
415
|
+
};
|
|
416
|
+
async function main(deps = defaultDeps) {
|
|
417
|
+
const { env } = deps;
|
|
418
|
+
const projectName = requireEnv(env, "PROJECT_NAME");
|
|
419
|
+
const [projectOwner, projectRepoName] = requireEnv(env, "PROJECT_REPO").split("/");
|
|
420
|
+
const [vaultOwner, vaultRepoName] = requireEnv(env, "VAULT_REPO").split("/");
|
|
421
|
+
const vaultOctokit = deps.createGithubClient(requireEnv(env, "DEVKEEP_GITHUB_TOKEN"));
|
|
422
|
+
const projectOctokit = deps.createGithubClient(requireEnv(env, "GITHUB_TOKEN"));
|
|
423
|
+
const claude = deps.createClaudeClient(requireEnv(env, "ANTHROPIC_API_KEY"));
|
|
424
|
+
const result = await deps.draftReleaseNotes({
|
|
425
|
+
projectOctokit,
|
|
426
|
+
vaultOctokit,
|
|
427
|
+
claude,
|
|
428
|
+
projectOwner,
|
|
429
|
+
projectRepoName,
|
|
430
|
+
vaultOwner,
|
|
431
|
+
vaultRepoName,
|
|
432
|
+
projectName,
|
|
433
|
+
tag: requireEnv(env, "TAG")
|
|
434
|
+
});
|
|
435
|
+
deps.log(`Drafted release notes: ${result.path} (previous tag: ${result.previousTag ?? "none"}, ${result.entryCount} entries)`);
|
|
436
|
+
}
|
|
437
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
438
|
+
main().catch((error) => {
|
|
439
|
+
console.error(error);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
export {
|
|
444
|
+
main,
|
|
445
|
+
requireEnv
|
|
446
|
+
};
|
package/dist/sync-onboarding.js
CHANGED