@starterculture/devkeep-actions 0.0.1
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/LICENSE +13 -0
- package/README.md +16 -0
- package/dist/consolidate-release.js +450 -0
- package/dist/crypting-candidates-scan.js +371 -0
- package/dist/draft-log-entry.js +718 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2026 StarterCulture. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This package is published solely so that Devkeep's own GitHub Actions
|
|
4
|
+
workflows can install and run it. No licence is granted to use, copy,
|
|
5
|
+
modify, distribute, or create derivative works from it for any other
|
|
6
|
+
purpose.
|
|
7
|
+
|
|
8
|
+
Publishing it to a public registry is a distribution mechanism, not a
|
|
9
|
+
grant of rights: a GitHub Actions runner is an ephemeral machine that can
|
|
10
|
+
only obtain code by fetching it from somewhere publicly readable, and
|
|
11
|
+
there is no way to make that fetch work for a repository owner without
|
|
12
|
+
making the artifact itself fetchable. Its being downloadable is a
|
|
13
|
+
consequence of that, and nothing more.
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @starterculture/devkeep-actions
|
|
2
|
+
|
|
3
|
+
Bundled entrypoints that Devkeep's GitHub Actions workflows install and
|
|
4
|
+
run on a CI runner.
|
|
5
|
+
|
|
6
|
+
This is an internal runtime component, not a library. It has no stable
|
|
7
|
+
public API, no documented interface, and no standalone use: each bundle
|
|
8
|
+
expects a specific set of environment variables supplied by the workflow
|
|
9
|
+
that invokes it, and writes its output to a Devkeep vault repository.
|
|
10
|
+
Contents and behaviour may change without notice between versions.
|
|
11
|
+
|
|
12
|
+
It is published to a public registry for one reason: a GitHub Actions
|
|
13
|
+
runner is an ephemeral machine that can only obtain code by fetching it
|
|
14
|
+
from somewhere publicly readable.
|
|
15
|
+
|
|
16
|
+
Licence: see LICENSE. All rights reserved.
|
|
@@ -0,0 +1,450 @@
|
|
|
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/ciUsage.js
|
|
127
|
+
var EMPTY_PROJECT_USAGE = {
|
|
128
|
+
draftLogEntry: ZERO_USAGE,
|
|
129
|
+
consolidateRelease: ZERO_USAGE,
|
|
130
|
+
syncUserManual: ZERO_USAGE,
|
|
131
|
+
rollover: ZERO_ROLLOVER_USAGE
|
|
132
|
+
};
|
|
133
|
+
function usagePath(projectName) {
|
|
134
|
+
return `projects/${projectName}/usage.json`;
|
|
135
|
+
}
|
|
136
|
+
async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
137
|
+
try {
|
|
138
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
139
|
+
owner,
|
|
140
|
+
repo,
|
|
141
|
+
path: usagePath(projectName)
|
|
142
|
+
});
|
|
143
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
144
|
+
return { usage: EMPTY_PROJECT_USAGE };
|
|
145
|
+
}
|
|
146
|
+
const parsed = JSON.parse(Buffer.from(data.content, "base64").toString("utf-8"));
|
|
147
|
+
const usage = { ...EMPTY_PROJECT_USAGE, ...parsed };
|
|
148
|
+
return { usage, sha: data.sha };
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (isNotFound(error)) {
|
|
151
|
+
return { usage: EMPTY_PROJECT_USAGE };
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
157
|
+
return {
|
|
158
|
+
...current,
|
|
159
|
+
[source]: addTokenUsage(current[source], delta),
|
|
160
|
+
rollover: addRolloverUsage(current.rollover, delta, now)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
164
|
+
const { owner, repo, projectName, branch } = params;
|
|
165
|
+
await retryOnConflict(async () => {
|
|
166
|
+
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
167
|
+
const updated = addUsage(current, source, delta, now);
|
|
168
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
169
|
+
owner,
|
|
170
|
+
repo,
|
|
171
|
+
path: usagePath(projectName),
|
|
172
|
+
message: `Record ${source} token usage: ${projectName}`,
|
|
173
|
+
content: Buffer.from(JSON.stringify(updated, null, 2), "utf-8").toString("base64"),
|
|
174
|
+
branch,
|
|
175
|
+
sha
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function isNotFound(error) {
|
|
180
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ../../modules/devlore/dist/masterDocs.js
|
|
184
|
+
function masterDocsPath(projectName) {
|
|
185
|
+
return `projects/${projectName}/master/README.md`;
|
|
186
|
+
}
|
|
187
|
+
async function getMasterDocs(octokit, params) {
|
|
188
|
+
return (await getMasterDocsFile(octokit, params)).content;
|
|
189
|
+
}
|
|
190
|
+
async function getMasterDocsSha(octokit, params) {
|
|
191
|
+
return (await getMasterDocsFile(octokit, params)).sha;
|
|
192
|
+
}
|
|
193
|
+
async function getMasterDocsFile(octokit, { owner, repo, projectName }) {
|
|
194
|
+
try {
|
|
195
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
196
|
+
owner,
|
|
197
|
+
repo,
|
|
198
|
+
path: masterDocsPath(projectName)
|
|
199
|
+
});
|
|
200
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
201
|
+
return { content: "" };
|
|
202
|
+
}
|
|
203
|
+
return { content: Buffer.from(data.content, "base64").toString("utf-8"), sha: data.sha };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (isNotFound2(error)) {
|
|
206
|
+
return { content: "" };
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function isNotFound2(error) {
|
|
212
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ../../modules/devlore/dist/prompt.js
|
|
216
|
+
function buildConsolidationPrompt(input) {
|
|
217
|
+
const entriesText = input.logEntries.map((entry) => `--- ${entry.path} ---
|
|
218
|
+
${entry.content}`).join("\n\n");
|
|
219
|
+
return [
|
|
220
|
+
`Project: ${input.projectName}`,
|
|
221
|
+
`Release: ${input.tag}`,
|
|
222
|
+
`Previous release: ${input.previousTag ?? "(none \u2014 this is the first release)"}`,
|
|
223
|
+
`Date: ${input.date}`,
|
|
224
|
+
"",
|
|
225
|
+
"Current master docs (may be empty if none exist yet):",
|
|
226
|
+
input.masterDocs || "(none yet)",
|
|
227
|
+
"",
|
|
228
|
+
"Log entries since the previous release:",
|
|
229
|
+
entriesText || "(none)",
|
|
230
|
+
"",
|
|
231
|
+
`Draft the updated master docs for ${input.projectName} as of ${input.tag}, folding in what changed across the log entries above into the current master docs. This is the official record, reviewed by a human before merging, so write it as durable reference documentation, not a changelog.`
|
|
232
|
+
].join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ../../modules/devlore/dist/logEntries.js
|
|
236
|
+
function logEntriesDir(projectName) {
|
|
237
|
+
return `projects/${projectName}/log`;
|
|
238
|
+
}
|
|
239
|
+
async function listLogEntries(octokit, { owner, repo, projectName }) {
|
|
240
|
+
const dir = logEntriesDir(projectName);
|
|
241
|
+
let files;
|
|
242
|
+
try {
|
|
243
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dir });
|
|
244
|
+
if (!Array.isArray(data)) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
files = data;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (isNotFound3(error)) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
const entries = await Promise.all(files.filter((file) => file.type === "file").map(async (file) => {
|
|
255
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: file.path });
|
|
256
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return { path: file.path, content: Buffer.from(data.content, "base64").toString("utf-8") };
|
|
260
|
+
}));
|
|
261
|
+
return entries.filter((entry) => entry !== null).sort((a, b) => a.path.localeCompare(b.path));
|
|
262
|
+
}
|
|
263
|
+
function isNotFound3(error) {
|
|
264
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ../../modules/devlore/dist/releaseScope.js
|
|
268
|
+
var TAG_PATTERN = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
269
|
+
function parseTag(tag) {
|
|
270
|
+
const match = tag.match(TAG_PATTERN);
|
|
271
|
+
if (!match) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
275
|
+
}
|
|
276
|
+
function compareTags(a, b) {
|
|
277
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
278
|
+
}
|
|
279
|
+
async function findPreviousTag(octokit, { owner, repo, currentTag }) {
|
|
280
|
+
const current = parseTag(currentTag);
|
|
281
|
+
if (!current) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const tags = await octokit.paginate(octokit.rest.repos.listTags, { owner, repo, per_page: 100 });
|
|
285
|
+
let best = null;
|
|
286
|
+
for (const tag of tags) {
|
|
287
|
+
const version = parseTag(tag.name);
|
|
288
|
+
if (!version || compareTags(version, current) >= 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!best || compareTags(version, best.version) > 0) {
|
|
292
|
+
best = { name: tag.name, version };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return best?.name ?? null;
|
|
296
|
+
}
|
|
297
|
+
async function listCommitShasSince(octokit, { owner, repo, previousTag, currentTag }) {
|
|
298
|
+
try {
|
|
299
|
+
const { data } = await octokit.rest.repos.compareCommitsWithBasehead({
|
|
300
|
+
owner,
|
|
301
|
+
repo,
|
|
302
|
+
basehead: `${previousTag}...${currentTag}`
|
|
303
|
+
});
|
|
304
|
+
return new Set(data.commits.map((commit) => commit.sha));
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function logEntryCommitSha(entry) {
|
|
310
|
+
const filename = entry.path.split("/").pop() ?? "";
|
|
311
|
+
return filename.replace(/\.md$/, "");
|
|
312
|
+
}
|
|
313
|
+
async function selectLogEntriesSincePreviousRelease({ vaultOctokit, projectOctokit, vaultOwner, vaultRepoName, projectName, projectOwner, projectRepoName, tag }) {
|
|
314
|
+
const allEntries = await listLogEntries(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName });
|
|
315
|
+
const previousTag = await findPreviousTag(projectOctokit, {
|
|
316
|
+
owner: projectOwner,
|
|
317
|
+
repo: projectRepoName,
|
|
318
|
+
currentTag: tag
|
|
319
|
+
});
|
|
320
|
+
if (!previousTag) {
|
|
321
|
+
return { logEntries: allEntries, previousTag: null };
|
|
322
|
+
}
|
|
323
|
+
const commitShas = await listCommitShasSince(projectOctokit, {
|
|
324
|
+
owner: projectOwner,
|
|
325
|
+
repo: projectRepoName,
|
|
326
|
+
previousTag,
|
|
327
|
+
currentTag: tag
|
|
328
|
+
});
|
|
329
|
+
if (!commitShas) {
|
|
330
|
+
return { logEntries: allEntries, previousTag };
|
|
331
|
+
}
|
|
332
|
+
return { logEntries: allEntries.filter((entry) => commitShas.has(logEntryCommitSha(entry))), previousTag };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ../../modules/devlore/dist/actions/consolidateRelease.js
|
|
336
|
+
var VAULT_BASE_BRANCH = "main";
|
|
337
|
+
var MODEL_TIER = "balanced";
|
|
338
|
+
var MAX_TOKENS = 2e4;
|
|
339
|
+
var MAX_CONTINUATIONS = 8;
|
|
340
|
+
function releaseBranchName(projectName, tag) {
|
|
341
|
+
return `devkeep/${projectName}/${tag}`;
|
|
342
|
+
}
|
|
343
|
+
async function consolidateRelease({ projectOctokit, vaultOctokit, claude, projectOwner, projectRepoName, vaultOwner, vaultRepoName, projectName, tag }) {
|
|
344
|
+
const [{ logEntries, previousTag }, masterDocs, masterDocsSha] = await Promise.all([
|
|
345
|
+
selectLogEntriesSincePreviousRelease({
|
|
346
|
+
vaultOctokit,
|
|
347
|
+
projectOctokit,
|
|
348
|
+
vaultOwner,
|
|
349
|
+
vaultRepoName,
|
|
350
|
+
projectName,
|
|
351
|
+
projectOwner,
|
|
352
|
+
projectRepoName,
|
|
353
|
+
tag
|
|
354
|
+
}),
|
|
355
|
+
getMasterDocs(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName }),
|
|
356
|
+
getMasterDocsSha(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName })
|
|
357
|
+
]);
|
|
358
|
+
const prompt = buildConsolidationPrompt({
|
|
359
|
+
projectName,
|
|
360
|
+
tag,
|
|
361
|
+
previousTag,
|
|
362
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
363
|
+
logEntries,
|
|
364
|
+
masterDocs
|
|
365
|
+
});
|
|
366
|
+
const { text: draftedDocs, usage } = await createCompleteMessage(claude, {
|
|
367
|
+
model: defaultModelFor(MODEL_TIER),
|
|
368
|
+
max_tokens: MAX_TOKENS,
|
|
369
|
+
messages: [{ role: "user", content: prompt }]
|
|
370
|
+
}, { maxContinuations: MAX_CONTINUATIONS });
|
|
371
|
+
const branch = releaseBranchName(projectName, tag);
|
|
372
|
+
const baseRef = await vaultOctokit.rest.git.getRef({
|
|
373
|
+
owner: vaultOwner,
|
|
374
|
+
repo: vaultRepoName,
|
|
375
|
+
ref: `heads/${VAULT_BASE_BRANCH}`
|
|
376
|
+
});
|
|
377
|
+
await vaultOctokit.rest.git.createRef({
|
|
378
|
+
owner: vaultOwner,
|
|
379
|
+
repo: vaultRepoName,
|
|
380
|
+
ref: `refs/heads/${branch}`,
|
|
381
|
+
sha: baseRef.data.object.sha
|
|
382
|
+
});
|
|
383
|
+
await vaultOctokit.rest.repos.createOrUpdateFileContents({
|
|
384
|
+
owner: vaultOwner,
|
|
385
|
+
repo: vaultRepoName,
|
|
386
|
+
path: masterDocsPath(projectName),
|
|
387
|
+
message: `Consolidate release: ${projectName}@${tag}`,
|
|
388
|
+
content: Buffer.from(draftedDocs, "utf-8").toString("base64"),
|
|
389
|
+
branch,
|
|
390
|
+
sha: masterDocsSha
|
|
391
|
+
});
|
|
392
|
+
await recordUsage(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName, branch }, "consolidateRelease", usage);
|
|
393
|
+
const pr = await vaultOctokit.rest.pulls.create({
|
|
394
|
+
owner: vaultOwner,
|
|
395
|
+
repo: vaultRepoName,
|
|
396
|
+
title: `Consolidate release: ${projectName}@${tag}`,
|
|
397
|
+
head: branch,
|
|
398
|
+
base: VAULT_BASE_BRANCH,
|
|
399
|
+
body: `Drafted master doc update for \`${projectName}\` at \`${tag}\`, generated from the log entries since the last release. Review before merging \u2014 this updates the official docs.`
|
|
400
|
+
});
|
|
401
|
+
return { branch, prNumber: pr.data.number, prUrl: pr.data.html_url, usage };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ../../modules/devlore/dist/bin/consolidateReleaseCli.js
|
|
405
|
+
function requireEnv(env, name) {
|
|
406
|
+
const value = env[name];
|
|
407
|
+
if (!value) {
|
|
408
|
+
throw new Error(`${name} is not set`);
|
|
409
|
+
}
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
var defaultDeps = {
|
|
413
|
+
env: process.env,
|
|
414
|
+
consolidateRelease,
|
|
415
|
+
createGithubClient,
|
|
416
|
+
createClaudeClient,
|
|
417
|
+
log: console.log
|
|
418
|
+
};
|
|
419
|
+
async function main(deps = defaultDeps) {
|
|
420
|
+
const { env } = deps;
|
|
421
|
+
const projectName = requireEnv(env, "PROJECT_NAME");
|
|
422
|
+
const [projectOwner, projectRepoName] = requireEnv(env, "PROJECT_REPO").split("/");
|
|
423
|
+
const [vaultOwner, vaultRepoName] = requireEnv(env, "VAULT_REPO").split("/");
|
|
424
|
+
const tag = requireEnv(env, "TAG");
|
|
425
|
+
const projectOctokit = deps.createGithubClient(requireEnv(env, "GITHUB_TOKEN"));
|
|
426
|
+
const vaultOctokit = deps.createGithubClient(requireEnv(env, "DEVKEEP_GITHUB_TOKEN"));
|
|
427
|
+
const claude = deps.createClaudeClient(requireEnv(env, "ANTHROPIC_API_KEY"));
|
|
428
|
+
const result = await deps.consolidateRelease({
|
|
429
|
+
projectOctokit,
|
|
430
|
+
vaultOctokit,
|
|
431
|
+
claude,
|
|
432
|
+
projectOwner,
|
|
433
|
+
projectRepoName,
|
|
434
|
+
vaultOwner,
|
|
435
|
+
vaultRepoName,
|
|
436
|
+
projectName,
|
|
437
|
+
tag
|
|
438
|
+
});
|
|
439
|
+
deps.log(`Opened consolidation PR #${result.prNumber}: ${result.prUrl}`);
|
|
440
|
+
}
|
|
441
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
442
|
+
main().catch((error) => {
|
|
443
|
+
console.error(error);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
export {
|
|
448
|
+
main,
|
|
449
|
+
requireEnv
|
|
450
|
+
};
|