@devinnn/docdrift 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -22
- package/dist/src/cli.js +13 -1
- package/dist/src/config/load.js +21 -1
- package/dist/src/config/normalize.js +68 -0
- package/dist/src/config/schema.js +55 -25
- package/dist/src/config/validate.js +3 -4
- package/dist/src/detect/docsCheck.js +4 -4
- package/dist/src/detect/heuristics.js +2 -2
- package/dist/src/detect/index.js +56 -47
- package/dist/src/detect/openapi.js +92 -9
- package/dist/src/devin/prompts.js +56 -5
- package/dist/src/devin/schemas.js +19 -19
- package/dist/src/devin/v1.js +8 -8
- package/dist/src/evidence/bundle.js +3 -3
- package/dist/src/github/client.js +77 -2
- package/dist/src/index.js +257 -154
- package/dist/src/model/state.js +1 -1
- package/dist/src/policy/confidence.js +1 -1
- package/dist/src/policy/engine.js +6 -5
- package/dist/src/utils/exec.js +2 -2
- package/dist/src/utils/glob.js +13 -0
- package/package.json +18 -5
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.detectOpenApiDrift = detectOpenApiDrift;
|
|
7
|
+
exports.detectOpenApiDriftFromNormalized = detectOpenApiDriftFromNormalized;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const exec_1 = require("../utils/exec");
|
|
@@ -56,7 +57,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
56
57
|
"\n--- stdout ---",
|
|
57
58
|
exportResult.stdout,
|
|
58
59
|
"\n--- stderr ---",
|
|
59
|
-
exportResult.stderr
|
|
60
|
+
exportResult.stderr,
|
|
60
61
|
].join("\n"), "utf8");
|
|
61
62
|
if (exportResult.exitCode !== 0) {
|
|
62
63
|
return {
|
|
@@ -67,8 +68,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
67
68
|
kind: "weak_evidence",
|
|
68
69
|
tier: 2,
|
|
69
70
|
confidence: 0.35,
|
|
70
|
-
evidence: [exportLogPath]
|
|
71
|
-
}
|
|
71
|
+
evidence: [exportLogPath],
|
|
72
|
+
},
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
if (!node_fs_1.default.existsSync(openapi.generatedPath) || !node_fs_1.default.existsSync(openapi.publishedPath)) {
|
|
@@ -80,8 +81,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
80
81
|
kind: "weak_evidence",
|
|
81
82
|
tier: 2,
|
|
82
83
|
confidence: 0.35,
|
|
83
|
-
evidence: [exportLogPath]
|
|
84
|
-
}
|
|
84
|
+
evidence: [exportLogPath],
|
|
85
|
+
},
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
const generatedRaw = node_fs_1.default.readFileSync(openapi.generatedPath, "utf8");
|
|
@@ -94,7 +95,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
94
95
|
return {
|
|
95
96
|
impactedDocs: [openapi.publishedPath],
|
|
96
97
|
evidenceFiles: [exportLogPath],
|
|
97
|
-
summary: "No OpenAPI drift detected"
|
|
98
|
+
summary: "No OpenAPI drift detected",
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
const summary = summarizeSpecDelta(publishedJson, generatedJson);
|
|
@@ -107,7 +108,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
107
108
|
normalizedPublished,
|
|
108
109
|
"",
|
|
109
110
|
"# Generated (normalized)",
|
|
110
|
-
normalizedGenerated
|
|
111
|
+
normalizedGenerated,
|
|
111
112
|
].join("\n"), "utf8");
|
|
112
113
|
return {
|
|
113
114
|
impactedDocs: [...new Set([openapi.publishedPath, ...(docArea.patch.targets ?? [])])],
|
|
@@ -117,7 +118,89 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
117
118
|
kind: "openapi_diff",
|
|
118
119
|
tier: 1,
|
|
119
120
|
confidence: 0.95,
|
|
120
|
-
evidence: [diffPath]
|
|
121
|
-
}
|
|
121
|
+
evidence: [diffPath],
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Run OpenAPI drift detection from normalized config (simple openapi block). Used as gate. */
|
|
126
|
+
async function detectOpenApiDriftFromNormalized(config, evidenceDir) {
|
|
127
|
+
const openapi = config.openapi;
|
|
128
|
+
const exportLogPath = node_path_1.default.join(evidenceDir, "openapi-export.log");
|
|
129
|
+
const exportResult = await (0, exec_1.execCommand)(openapi.export);
|
|
130
|
+
node_fs_1.default.writeFileSync(exportLogPath, [
|
|
131
|
+
`$ ${openapi.export}`,
|
|
132
|
+
`exitCode: ${exportResult.exitCode}`,
|
|
133
|
+
"\n--- stdout ---",
|
|
134
|
+
exportResult.stdout,
|
|
135
|
+
"\n--- stderr ---",
|
|
136
|
+
exportResult.stderr,
|
|
137
|
+
].join("\n"), "utf8");
|
|
138
|
+
if (exportResult.exitCode !== 0) {
|
|
139
|
+
return {
|
|
140
|
+
impactedDocs: [openapi.published],
|
|
141
|
+
evidenceFiles: [exportLogPath],
|
|
142
|
+
summary: "OpenAPI export command failed",
|
|
143
|
+
signal: {
|
|
144
|
+
kind: "weak_evidence",
|
|
145
|
+
tier: 2,
|
|
146
|
+
confidence: 0.35,
|
|
147
|
+
evidence: [exportLogPath],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!node_fs_1.default.existsSync(openapi.generated) || !node_fs_1.default.existsSync(openapi.published)) {
|
|
152
|
+
return {
|
|
153
|
+
impactedDocs: [openapi.generated, openapi.published],
|
|
154
|
+
evidenceFiles: [exportLogPath],
|
|
155
|
+
summary: "OpenAPI file(s) missing",
|
|
156
|
+
signal: {
|
|
157
|
+
kind: "weak_evidence",
|
|
158
|
+
tier: 2,
|
|
159
|
+
confidence: 0.35,
|
|
160
|
+
evidence: [exportLogPath],
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const generatedRaw = node_fs_1.default.readFileSync(openapi.generated, "utf8");
|
|
165
|
+
const publishedRaw = node_fs_1.default.readFileSync(openapi.published, "utf8");
|
|
166
|
+
const generatedJson = JSON.parse(generatedRaw);
|
|
167
|
+
const publishedJson = JSON.parse(publishedRaw);
|
|
168
|
+
const normalizedGenerated = (0, json_1.stableStringify)(generatedJson);
|
|
169
|
+
const normalizedPublished = (0, json_1.stableStringify)(publishedJson);
|
|
170
|
+
if (normalizedGenerated === normalizedPublished) {
|
|
171
|
+
return {
|
|
172
|
+
impactedDocs: [openapi.published],
|
|
173
|
+
evidenceFiles: [exportLogPath],
|
|
174
|
+
summary: "No OpenAPI drift detected",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const summary = summarizeSpecDelta(publishedJson, generatedJson);
|
|
178
|
+
const diffPath = node_path_1.default.join(evidenceDir, "openapi.diff.txt");
|
|
179
|
+
node_fs_1.default.writeFileSync(diffPath, [
|
|
180
|
+
"# OpenAPI Drift Summary",
|
|
181
|
+
summary,
|
|
182
|
+
"",
|
|
183
|
+
"# Published (normalized)",
|
|
184
|
+
normalizedPublished,
|
|
185
|
+
"",
|
|
186
|
+
"# Generated (normalized)",
|
|
187
|
+
normalizedGenerated,
|
|
188
|
+
].join("\n"), "utf8");
|
|
189
|
+
const impactedDocs = [
|
|
190
|
+
...new Set([
|
|
191
|
+
openapi.published,
|
|
192
|
+
...config.docAreas.flatMap((a) => a.patch.targets ?? []).filter(Boolean),
|
|
193
|
+
]),
|
|
194
|
+
].filter(Boolean);
|
|
195
|
+
return {
|
|
196
|
+
impactedDocs,
|
|
197
|
+
evidenceFiles: [exportLogPath, diffPath],
|
|
198
|
+
summary,
|
|
199
|
+
signal: {
|
|
200
|
+
kind: "openapi_diff",
|
|
201
|
+
tier: 1,
|
|
202
|
+
confidence: 0.95,
|
|
203
|
+
evidence: [diffPath],
|
|
204
|
+
},
|
|
122
205
|
};
|
|
123
206
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildAutogenPrompt = buildAutogenPrompt;
|
|
4
4
|
exports.buildConceptualPrompt = buildConceptualPrompt;
|
|
5
|
+
exports.buildWholeDocsitePrompt = buildWholeDocsitePrompt;
|
|
5
6
|
function attachmentBlock(attachmentUrls) {
|
|
6
7
|
return attachmentUrls.map((url, index) => `- ATTACHMENT ${index + 1}: ${url}`).join("\n");
|
|
7
8
|
}
|
|
8
9
|
function buildAutogenPrompt(input) {
|
|
9
|
-
|
|
10
|
+
const base = [
|
|
10
11
|
"You are Devin. Task: update API reference docs to match actual code/spec changes.",
|
|
11
12
|
"",
|
|
12
13
|
"EVIDENCE (attachments):",
|
|
@@ -30,12 +31,16 @@ function buildAutogenPrompt(input) {
|
|
|
30
31
|
" e) get blocked (status=BLOCKED + questions),",
|
|
31
32
|
" f) complete (status=DONE).",
|
|
32
33
|
"",
|
|
33
|
-
`Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence
|
|
34
|
+
`Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`,
|
|
34
35
|
].join("\n");
|
|
36
|
+
if (input.customAppend) {
|
|
37
|
+
return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
|
|
38
|
+
}
|
|
39
|
+
return base;
|
|
35
40
|
}
|
|
36
41
|
function buildConceptualPrompt(input) {
|
|
37
|
-
|
|
38
|
-
"You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes.",
|
|
42
|
+
const base = [
|
|
43
|
+
"You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes. i..e GUIDES",
|
|
39
44
|
"",
|
|
40
45
|
"EVIDENCE (attachments):",
|
|
41
46
|
attachmentBlock(input.attachmentUrls),
|
|
@@ -50,6 +55,52 @@ function buildConceptualPrompt(input) {
|
|
|
50
55
|
"- Update at planning, editing, verifying, open-pr, blocked, done milestones.",
|
|
51
56
|
"- If blocked, fill blocked.questions with concrete, reviewer-actionable questions.",
|
|
52
57
|
"",
|
|
53
|
-
"Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text."
|
|
58
|
+
"Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text.",
|
|
54
59
|
].join("\n");
|
|
60
|
+
if (input.customAppend) {
|
|
61
|
+
return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
|
|
62
|
+
}
|
|
63
|
+
return base;
|
|
64
|
+
}
|
|
65
|
+
/** Whole-docsite prompt for single-session runs */
|
|
66
|
+
function buildWholeDocsitePrompt(input) {
|
|
67
|
+
const excludeNote = input.config.exclude?.length > 0
|
|
68
|
+
? `\n6) NEVER modify files matching these patterns: ${input.config.exclude.join(", ")}`
|
|
69
|
+
: "";
|
|
70
|
+
const requireReviewNote = input.config.requireHumanReview?.length > 0
|
|
71
|
+
? `\n7) If you touch files under: ${input.config.requireHumanReview.join(", ")} — note it in the PR description (a follow-up issue will flag for human review).`
|
|
72
|
+
: "";
|
|
73
|
+
const allowNewFiles = input.config.policy.allowNewFiles ?? false;
|
|
74
|
+
const newFilesRule = allowNewFiles
|
|
75
|
+
? "8) You MAY add new articles, create new folders, and change information architecture when warranted."
|
|
76
|
+
: "8) You may ONLY edit existing files. Do NOT create new files, new articles, or new folders. Do NOT change information architecture.";
|
|
77
|
+
const base = [
|
|
78
|
+
"You are Devin. Task: update the entire docsite to match the API and code changes.",
|
|
79
|
+
"",
|
|
80
|
+
"EVIDENCE (attachments):",
|
|
81
|
+
input.attachmentUrls.map((url, i) => `- ATTACHMENT ${i + 1}: ${url}`).join("\n"),
|
|
82
|
+
"",
|
|
83
|
+
"Rules (hard):",
|
|
84
|
+
`1) Only modify files under: ${input.config.policy.allowlist.join(", ")}`,
|
|
85
|
+
"2) Make the smallest change that makes docs correct.",
|
|
86
|
+
"3) Update API reference (OpenAPI) and any impacted guides in one PR.",
|
|
87
|
+
"4) Run verification commands and record results:",
|
|
88
|
+
...input.config.policy.verification.commands.map((c) => ` - ${c}`),
|
|
89
|
+
"5) Open exactly ONE pull request with a clear title and reviewer-friendly description.",
|
|
90
|
+
`6) Docsite scope: ${input.config.docsite.join(", ")}` +
|
|
91
|
+
excludeNote +
|
|
92
|
+
requireReviewNote +
|
|
93
|
+
`\n${newFilesRule}`,
|
|
94
|
+
"",
|
|
95
|
+
"Structured Output:",
|
|
96
|
+
"- Maintain structured output in the provided JSON schema.",
|
|
97
|
+
"- Update it at: planning, editing, verifying, open-pr, blocked, done.",
|
|
98
|
+
"- If blocked, fill blocked.questions with concrete questions.",
|
|
99
|
+
"",
|
|
100
|
+
"Goal: Produce ONE PR that updates the whole docsite (API reference + guides) using only the evidence.",
|
|
101
|
+
].join("\n");
|
|
102
|
+
if (input.config.devin.customInstructionContent) {
|
|
103
|
+
return base + "\n\n---\n\nCustom instructions:\n\n" + input.config.devin.customInstructionContent;
|
|
104
|
+
}
|
|
105
|
+
return base;
|
|
55
106
|
}
|
|
@@ -13,7 +13,7 @@ exports.PatchPlanSchema = {
|
|
|
13
13
|
"evidence",
|
|
14
14
|
"filesToEdit",
|
|
15
15
|
"verification",
|
|
16
|
-
"nextAction"
|
|
16
|
+
"nextAction",
|
|
17
17
|
],
|
|
18
18
|
properties: {
|
|
19
19
|
status: { enum: ["PLANNING", "EDITING", "VERIFYING", "OPENED_PR", "BLOCKED", "DONE"] },
|
|
@@ -27,8 +27,8 @@ exports.PatchPlanSchema = {
|
|
|
27
27
|
required: ["attachments", "diffSummary"],
|
|
28
28
|
properties: {
|
|
29
29
|
attachments: { type: "array", items: { type: "string" } },
|
|
30
|
-
diffSummary: { type: "string" }
|
|
31
|
-
}
|
|
30
|
+
diffSummary: { type: "string" },
|
|
31
|
+
},
|
|
32
32
|
},
|
|
33
33
|
filesToEdit: { type: "array", items: { type: "string" } },
|
|
34
34
|
verification: {
|
|
@@ -37,8 +37,8 @@ exports.PatchPlanSchema = {
|
|
|
37
37
|
required: ["commands"],
|
|
38
38
|
properties: {
|
|
39
39
|
commands: { type: "array", items: { type: "string" } },
|
|
40
|
-
results: { type: "array", items: { type: "string" } }
|
|
41
|
-
}
|
|
40
|
+
results: { type: "array", items: { type: "string" } },
|
|
41
|
+
},
|
|
42
42
|
},
|
|
43
43
|
nextAction: { enum: ["OPEN_PR", "OPEN_ISSUE", "NOOP"] },
|
|
44
44
|
pr: {
|
|
@@ -46,18 +46,18 @@ exports.PatchPlanSchema = {
|
|
|
46
46
|
additionalProperties: false,
|
|
47
47
|
properties: {
|
|
48
48
|
title: { type: "string" },
|
|
49
|
-
url: { type: "string" }
|
|
50
|
-
}
|
|
49
|
+
url: { type: "string" },
|
|
50
|
+
},
|
|
51
51
|
},
|
|
52
52
|
blocked: {
|
|
53
53
|
type: "object",
|
|
54
54
|
additionalProperties: false,
|
|
55
55
|
properties: {
|
|
56
56
|
reason: { type: "string" },
|
|
57
|
-
questions: { type: "array", items: { type: "string" } }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
57
|
+
questions: { type: "array", items: { type: "string" } },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
61
|
};
|
|
62
62
|
exports.PatchResultSchema = {
|
|
63
63
|
type: "object",
|
|
@@ -74,8 +74,8 @@ exports.PatchResultSchema = {
|
|
|
74
74
|
required: ["commands", "results"],
|
|
75
75
|
properties: {
|
|
76
76
|
commands: { type: "array", items: { type: "string" } },
|
|
77
|
-
results: { type: "array", items: { type: "string" } }
|
|
78
|
-
}
|
|
77
|
+
results: { type: "array", items: { type: "string" } },
|
|
78
|
+
},
|
|
79
79
|
},
|
|
80
80
|
links: {
|
|
81
81
|
type: "object",
|
|
@@ -84,16 +84,16 @@ exports.PatchResultSchema = {
|
|
|
84
84
|
properties: {
|
|
85
85
|
sessionUrl: { type: "string" },
|
|
86
86
|
prUrl: { type: "string" },
|
|
87
|
-
issueUrl: { type: "string" }
|
|
88
|
-
}
|
|
87
|
+
issueUrl: { type: "string" },
|
|
88
|
+
},
|
|
89
89
|
},
|
|
90
90
|
blocked: {
|
|
91
91
|
type: "object",
|
|
92
92
|
additionalProperties: false,
|
|
93
93
|
properties: {
|
|
94
94
|
reason: { type: "string" },
|
|
95
|
-
questions: { type: "array", items: { type: "string" } }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
95
|
+
questions: { type: "array", items: { type: "string" } },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
99
|
};
|
package/dist/src/devin/v1.js
CHANGED
|
@@ -24,9 +24,9 @@ async function devinUploadAttachment(apiKey, filePath) {
|
|
|
24
24
|
const response = await fetch("https://api.devin.ai/v1/attachments", {
|
|
25
25
|
method: "POST",
|
|
26
26
|
headers: {
|
|
27
|
-
Authorization: `Bearer ${apiKey}
|
|
27
|
+
Authorization: `Bearer ${apiKey}`,
|
|
28
28
|
},
|
|
29
|
-
body: form
|
|
29
|
+
body: form,
|
|
30
30
|
});
|
|
31
31
|
const text = await response.text();
|
|
32
32
|
ensureOk(response, text, "Upload attachment");
|
|
@@ -49,9 +49,9 @@ async function devinCreateSession(apiKey, body) {
|
|
|
49
49
|
method: "POST",
|
|
50
50
|
headers: {
|
|
51
51
|
Authorization: `Bearer ${apiKey}`,
|
|
52
|
-
"Content-Type": "application/json"
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
53
|
},
|
|
54
|
-
body: JSON.stringify(body)
|
|
54
|
+
body: JSON.stringify(body),
|
|
55
55
|
});
|
|
56
56
|
const text = await response.text();
|
|
57
57
|
ensureOk(response, text, "Create session");
|
|
@@ -60,8 +60,8 @@ async function devinCreateSession(apiKey, body) {
|
|
|
60
60
|
async function devinGetSession(apiKey, sessionId) {
|
|
61
61
|
const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, {
|
|
62
62
|
headers: {
|
|
63
|
-
Authorization: `Bearer ${apiKey}
|
|
64
|
-
}
|
|
63
|
+
Authorization: `Bearer ${apiKey}`,
|
|
64
|
+
},
|
|
65
65
|
});
|
|
66
66
|
const text = await response.text();
|
|
67
67
|
ensureOk(response, text, "Get session");
|
|
@@ -77,8 +77,8 @@ async function devinListSessions(apiKey, params = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
const response = await fetch(url, {
|
|
79
79
|
headers: {
|
|
80
|
-
Authorization: `Bearer ${apiKey}
|
|
81
|
-
}
|
|
80
|
+
Authorization: `Bearer ${apiKey}`,
|
|
81
|
+
},
|
|
82
82
|
});
|
|
83
83
|
const text = await response.text();
|
|
84
84
|
ensureOk(response, text, "List sessions");
|
|
@@ -52,7 +52,7 @@ async function buildEvidenceBundle(input) {
|
|
|
52
52
|
baseSha: input.runInfo.baseSha,
|
|
53
53
|
headSha: input.runInfo.headSha,
|
|
54
54
|
trigger: input.runInfo.trigger,
|
|
55
|
-
timestamp: input.runInfo.timestamp
|
|
55
|
+
timestamp: input.runInfo.timestamp,
|
|
56
56
|
},
|
|
57
57
|
docArea: input.item.docArea,
|
|
58
58
|
mode: input.item.mode,
|
|
@@ -60,7 +60,7 @@ async function buildEvidenceBundle(input) {
|
|
|
60
60
|
signals: input.item.signals,
|
|
61
61
|
impactedDocs: input.item.impactedDocs,
|
|
62
62
|
copiedEvidence,
|
|
63
|
-
copiedDocs
|
|
63
|
+
copiedDocs,
|
|
64
64
|
});
|
|
65
65
|
const archivePath = `${bundleDir}.tar.gz`;
|
|
66
66
|
const parent = node_path_1.default.dirname(bundleDir);
|
|
@@ -73,7 +73,7 @@ async function buildEvidenceBundle(input) {
|
|
|
73
73
|
bundleDir,
|
|
74
74
|
archivePath,
|
|
75
75
|
manifestPath,
|
|
76
|
-
attachmentPaths: [archivePath, manifestPath]
|
|
76
|
+
attachmentPaths: [archivePath, manifestPath],
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
function writeMetrics(metrics) {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRepo = parseRepo;
|
|
3
4
|
exports.postCommitComment = postCommitComment;
|
|
4
5
|
exports.createIssue = createIssue;
|
|
5
6
|
exports.renderRunComment = renderRunComment;
|
|
6
7
|
exports.renderBlockedIssueBody = renderBlockedIssueBody;
|
|
8
|
+
exports.renderRequireHumanReviewIssueBody = renderRequireHumanReviewIssueBody;
|
|
9
|
+
exports.renderSlaIssueBody = renderSlaIssueBody;
|
|
10
|
+
exports.isPrOpen = isPrOpen;
|
|
11
|
+
exports.listOpenPrsWithLabel = listOpenPrsWithLabel;
|
|
7
12
|
const rest_1 = require("@octokit/rest");
|
|
8
13
|
function parseRepo(full) {
|
|
9
14
|
const [owner, repo] = full.split("/");
|
|
@@ -19,7 +24,7 @@ async function postCommitComment(input) {
|
|
|
19
24
|
owner,
|
|
20
25
|
repo,
|
|
21
26
|
commit_sha: input.commitSha,
|
|
22
|
-
body: input.body
|
|
27
|
+
body: input.body,
|
|
23
28
|
});
|
|
24
29
|
return response.data.html_url;
|
|
25
30
|
}
|
|
@@ -31,7 +36,7 @@ async function createIssue(input) {
|
|
|
31
36
|
repo,
|
|
32
37
|
title: input.issue.title,
|
|
33
38
|
body: input.issue.body,
|
|
34
|
-
labels: input.issue.labels
|
|
39
|
+
labels: input.issue.labels,
|
|
35
40
|
});
|
|
36
41
|
return response.data.html_url;
|
|
37
42
|
}
|
|
@@ -84,3 +89,73 @@ function renderBlockedIssueBody(input) {
|
|
|
84
89
|
}
|
|
85
90
|
return lines.join("\n");
|
|
86
91
|
}
|
|
92
|
+
function renderRequireHumanReviewIssueBody(input) {
|
|
93
|
+
const lines = [];
|
|
94
|
+
lines.push("## Why this issue");
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push("This doc-drift PR touches paths that require human review (guides, prose, or other non-technical docs).");
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push("## What to do");
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(`1. Review the PR: ${input.prUrl}`);
|
|
101
|
+
lines.push("2. Confirm the changes are correct or request modifications.");
|
|
102
|
+
lines.push("3. Merge or close the PR.");
|
|
103
|
+
lines.push("");
|
|
104
|
+
if (input.touchedPaths.length > 0) {
|
|
105
|
+
lines.push("## Touched paths (require review)");
|
|
106
|
+
lines.push("");
|
|
107
|
+
for (const p of input.touchedPaths.slice(0, 20)) {
|
|
108
|
+
lines.push(`- \`${p}\``);
|
|
109
|
+
}
|
|
110
|
+
if (input.touchedPaths.length > 20) {
|
|
111
|
+
lines.push(`- ... and ${input.touchedPaths.length - 20} more`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
function renderSlaIssueBody(input) {
|
|
117
|
+
const lines = [];
|
|
118
|
+
lines.push("## Why this issue");
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push(`Doc-drift PR(s) have been open for ${input.slaDays}+ days. Docs may be out of sync.`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push("## What to do");
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push("Please review and merge or close the following PR(s):");
|
|
125
|
+
lines.push("");
|
|
126
|
+
for (const url of input.prUrls) {
|
|
127
|
+
lines.push(`- ${url}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push("If the PR is no longer needed, close it to resolve this reminder.");
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
/** Check if a PR is still open. URL format: https://github.com/owner/repo/pull/123 */
|
|
134
|
+
async function isPrOpen(token, prUrl) {
|
|
135
|
+
const match = prUrl.match(/github\.com[/]([^/]+)[/]([^/]+)[/]pull[/](\d+)/);
|
|
136
|
+
if (!match)
|
|
137
|
+
return { open: false };
|
|
138
|
+
const [, owner, repo, numStr] = match;
|
|
139
|
+
const number = parseInt(numStr ?? "0", 10);
|
|
140
|
+
if (!owner || !repo || !Number.isFinite(number))
|
|
141
|
+
return { open: false };
|
|
142
|
+
const octokit = new rest_1.Octokit({ auth: token });
|
|
143
|
+
const { data } = await octokit.pulls.get({ owner, repo, pull_number: number });
|
|
144
|
+
return { open: data.state === "open", number: data.number };
|
|
145
|
+
}
|
|
146
|
+
/** List open PRs with a given label */
|
|
147
|
+
async function listOpenPrsWithLabel(token, repository, label) {
|
|
148
|
+
const octokit = new rest_1.Octokit({ auth: token });
|
|
149
|
+
const { owner, repo } = parseRepo(repository);
|
|
150
|
+
const { data } = await octokit.pulls.list({
|
|
151
|
+
owner,
|
|
152
|
+
repo,
|
|
153
|
+
state: "open",
|
|
154
|
+
labels: label,
|
|
155
|
+
});
|
|
156
|
+
return data.map((pr) => ({
|
|
157
|
+
url: pr.html_url ?? "",
|
|
158
|
+
number: pr.number,
|
|
159
|
+
created_at: pr.created_at ?? "",
|
|
160
|
+
}));
|
|
161
|
+
}
|