Skip to content

Commit 8a2f0a2

Browse files
committed
add dryRun param + some debug output + tests
1 parent 97d09ce commit 8a2f0a2

File tree

10 files changed

+559
-79
lines changed

10 files changed

+559
-79
lines changed

dist/index.js

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const parseInputs = () => {
1919
rootIssue,
2020
accessToken: "",
2121
sectionTitle: "Spec graph",
22+
dryRun: false
2223
};
2324
};
2425
exports.parseInputs = parseInputs;
@@ -96,8 +97,8 @@ class GraphBuilder {
9697
getGraph() {
9798
const graphNodes = Array.from(this.nodes.values());
9899
const vertices = graphNodes.map(x => x.value).filter((x) => x !== null);
99-
const startNode = mermaid_node_1.MermaidNode.getStartNode();
100-
const finishNode = mermaid_node_1.MermaidNode.getFinishNode();
100+
const startNode = mermaid_node_1.MermaidNode.createStartNode();
101+
const finishNode = mermaid_node_1.MermaidNode.createFinishNode();
101102
const edgesFromStartNode = graphNodes
102103
.filter(x => x.predecessors.length === 0)
103104
.map(x => ({ from: startNode, to: x.value }));
@@ -130,49 +131,58 @@ exports.IssueContentParser = void 0;
130131
const utils_1 = __nccwpck_require__(918);
131132
class IssueContentParser {
132133
extractIssueTasklist(issue) {
133-
var _a;
134-
const issueContent = (_a = issue.body) !== null && _a !== void 0 ? _a : "";
135-
const issueContentLines = issueContent.split("\n");
136-
return issueContentLines
137-
.filter(x => x.startsWith("- [ ] "))
134+
var _a, _b;
135+
const contentLines = (_b = (_a = issue.body) === null || _a === void 0 ? void 0 : _a.split("\n")) !== null && _b !== void 0 ? _b : [];
136+
return contentLines
137+
.filter(x => this.isTaskListLine(x))
138138
.map(x => (0, utils_1.parseIssueUrl)(x))
139139
.filter((x) => x !== null);
140140
}
141141
extractIssueDependencies(issue) {
142-
var _a;
143-
const issueContent = (_a = issue.body) !== null && _a !== void 0 ? _a : "";
144-
const issueContentLines = issueContent.split("\n");
145-
return issueContentLines
146-
.filter(x => x.startsWith("Depends on"))
147-
.map(x => x.split(",").map(y => (0, utils_1.parseIssueUrl)(y)))
142+
var _a, _b;
143+
const contentLines = (_b = (_a = issue.body) === null || _a === void 0 ? void 0 : _a.split("\n")) !== null && _b !== void 0 ? _b : [];
144+
return contentLines
145+
.filter(x => this.isDependencyLine(x))
146+
.map(x => (0, utils_1.parseIssuesUrls)(x))
148147
.flat()
149148
.filter((x) => x !== null);
150149
}
151150
replaceIssueContent(issue, sectionTitle, newSectionContent) {
152-
var _a;
153-
const content = (_a = issue.body) !== null && _a !== void 0 ? _a : "";
154-
const contentLines = content.split("\n");
155-
const sectionStartIndex = contentLines.findIndex(line => this.isLineMarkdownHeader(line, sectionTitle));
151+
var _a, _b;
152+
const contentLines = (_b = (_a = issue.body) === null || _a === void 0 ? void 0 : _a.split("\n")) !== null && _b !== void 0 ? _b : [];
153+
const sectionStartIndex = contentLines.findIndex(x => this.isMarkdownHeaderLine(x, sectionTitle));
156154
if (sectionStartIndex === -1) {
157-
throw "";
155+
throw `Markdown header '${sectionTitle}' is not found in issue body:\n ${issue.body}`;
158156
}
159-
const sectionEndIndex = contentLines.findIndex((line, lineIndex) => lineIndex > sectionStartIndex && this.isLineMarkdownHeader(line));
157+
const sectionEndIndex = contentLines.findIndex((x, index) => index > sectionStartIndex && this.isMarkdownHeaderLine(x));
160158
return [
161-
...contentLines.slice(0, sectionStartIndex),
159+
...contentLines.slice(0, sectionStartIndex + 1),
162160
newSectionContent,
163-
...contentLines.slice(sectionEndIndex),
161+
"",
162+
...contentLines.slice(sectionEndIndex !== -1 ? sectionEndIndex : contentLines.length),
164163
].join("\n");
165164
}
166-
isLineMarkdownHeader(line, sectionTitle) {
167-
if (!line.startsWith("#")) {
165+
isMarkdownHeaderLine(str, sectionTitle) {
166+
if (!str.startsWith("#")) {
167+
return false;
168+
}
169+
const trimmedLine = str.replace(/^#+/, "").trim();
170+
if (!trimmedLine) {
168171
return false;
169172
}
170173
if (!sectionTitle) {
171174
return true;
172175
}
173-
const trimmedLine = line.replace(/^#+/, "").trim();
174176
return trimmedLine.toLowerCase() === sectionTitle.toLocaleLowerCase();
175177
}
178+
isTaskListLine(str) {
179+
return str.startsWith("- [ ] ");
180+
}
181+
isDependencyLine(str) {
182+
const dependencyLinePrefixes = ["Dependencies: ", "Predecessors: ", "Depends on ", "Depends on: "];
183+
const formattedLine = str.toLowerCase();
184+
return dependencyLinePrefixes.some(x => formattedLine.startsWith(x.toLowerCase()));
185+
}
176186
}
177187
exports.IssueContentParser = IssueContentParser;
178188

@@ -223,19 +233,32 @@ const run = async () => {
223233
const mermaidRender = new mermaid_render_1.MermaidRender();
224234
const rootIssue = await githubApiClient.getIssue(config.rootIssue);
225235
const rootIssueTasklist = issueContentParser.extractIssueTasklist(rootIssue);
236+
core.info(`Found ${rootIssueTasklist.length} work items in task list.`);
237+
core.info("Building dependency graph...");
226238
const graphBuilder = new graph_builder_1.GraphBuilder();
227239
for (const issueRef of rootIssueTasklist) {
228240
const issue = await githubApiClient.getIssue(issueRef);
229-
const issueDetails = mermaid_node_1.MermaidNode.fromGitHubIssue(issue);
230-
graphBuilder.addIssue(issueRef, issueDetails);
241+
const issueDetails = mermaid_node_1.MermaidNode.createFromGitHubIssue(issue);
231242
const issueDependencies = issueContentParser.extractIssueDependencies(issue);
243+
graphBuilder.addIssue(issueRef, issueDetails);
232244
issueDependencies.forEach(x => graphBuilder.addDependency(x, issueRef));
233245
}
234246
const graph = graphBuilder.getGraph();
235247
const renderedContent = mermaidRender.render(graph);
236-
console.log(renderedContent);
248+
core.startGroup("Mermaid diagram");
249+
core.info(renderedContent);
250+
core.endGroup();
237251
const updatedIssueContent = issueContentParser.replaceIssueContent(rootIssue, config.sectionTitle, renderedContent);
252+
core.startGroup("Updated issue content");
253+
core.info(updatedIssueContent);
254+
core.endGroup();
255+
if (config.dryRun) {
256+
console.log("Action is run in dry-run mode. Root issue won't be updated");
257+
return;
258+
}
259+
core.info("Updating root issue...");
238260
await githubApiClient.updateIssueContent(config.rootIssue, updatedIssueContent);
261+
core.info("Root issue is updated.");
239262
}
240263
catch (error) {
241264
if (error instanceof Error) {
@@ -263,7 +286,7 @@ class MermaidNode {
263286
this.status = status;
264287
this.url = url;
265288
}
266-
static fromGitHubIssue(issue) {
289+
static createFromGitHubIssue(issue) {
267290
return new MermaidNode(`issue${issue.id}`, issue.title, MermaidNode.getNodeStatusFromGitHubIssue(issue), issue.html_url);
268291
}
269292
static getNodeStatusFromGitHubIssue(issue) {
@@ -275,10 +298,10 @@ class MermaidNode {
275298
}
276299
return "notstarted";
277300
}
278-
static getStartNode() {
301+
static createStartNode() {
279302
return new MermaidNode("start", "Start", "notstarted");
280303
}
281-
static getFinishNode() {
304+
static createFinishNode() {
282305
return new MermaidNode("finish", "Finish", "notstarted");
283306
}
284307
}
@@ -304,7 +327,7 @@ ${this.renderLegendSection()}
304327
${this.renderIssuesSection(graph.vertices)}
305328
${this.renderDependencies(graph.edges)}
306329
\`\`\`
307-
`;
330+
`;
308331
}
309332
renderCssSection() {
310333
return `
@@ -315,7 +338,7 @@ classDef started fill:#fae17d,color:#000;
315338
classDef completed fill:#ccffd8,color:#000;
316339

317340
%% </CSS>
318-
`;
341+
`;
319342
}
320343
renderLegendSection() {
321344
return `
@@ -330,7 +353,7 @@ subgraph legend["Legend"]
330353
end
331354

332355
%% </Legend>
333-
`;
356+
`;
334357
}
335358
renderIssuesSection(issues) {
336359
const renderedGraphIssues = issues.map(x => this.renderIssue(x)).join("\n\n");
@@ -340,7 +363,7 @@ end
340363
${renderedGraphIssues}
341364

342365
%% </Issues>
343-
`;
366+
`;
344367
}
345368
renderIssue(issue) {
346369
let result = `${issue.nodeId}("${issue.title}"):::${issue.status};`;
@@ -357,7 +380,7 @@ ${renderedGraphIssues}
357380
${renderedDependencies}
358381

359382
%% </Dependencies>
360-
`;
383+
`;
361384
}
362385
renderDependency(dependency) {
363386
return `${dependency.from.nodeId} --> ${dependency.to.nodeId};`;
@@ -374,10 +397,11 @@ exports.MermaidRender = MermaidRender;
374397
"use strict";
375398

376399
Object.defineProperty(exports, "__esModule", ({ value: true }));
377-
exports.parseIssueUrl = void 0;
400+
exports.parseIssuesUrls = exports.parseIssueUrl = void 0;
378401
const issueUrlRegex = /github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/i;
379-
const parseIssueUrl = (issueUrl) => {
380-
const found = issueUrl.match(issueUrlRegex);
402+
const issueUrlsRegex = new RegExp(issueUrlRegex, "ig");
403+
const parseIssueUrl = (str) => {
404+
const found = str.match(issueUrlRegex);
381405
if (!found) {
382406
return null;
383407
}
@@ -388,6 +412,18 @@ const parseIssueUrl = (issueUrl) => {
388412
};
389413
};
390414
exports.parseIssueUrl = parseIssueUrl;
415+
const parseIssuesUrls = (str) => {
416+
const result = [];
417+
for (const match of str.matchAll(issueUrlsRegex)) {
418+
result.push({
419+
repoOwner: match[1],
420+
repoName: match[2],
421+
issueNumber: parseInt(match[3]),
422+
});
423+
}
424+
return result;
425+
};
426+
exports.parseIssuesUrls = parseIssuesUrls;
391427

392428

393429
/***/ }),

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Config {
55
rootIssue: GitHubIssueReference;
66
sectionTitle: string;
77
accessToken: string;
8+
dryRun: boolean;
89
}
910

1011
export const parseInputs = (): Config => {
@@ -18,5 +19,6 @@ export const parseInputs = (): Config => {
1819
rootIssue,
1920
accessToken: "",
2021
sectionTitle: "Spec graph",
22+
dryRun: false
2123
};
2224
};

src/graph-builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ export class GraphBuilder {
5555
const graphNodes = Array.from(this.nodes.values());
5656
const vertices = graphNodes.map(x => x.value).filter((x): x is MermaidNode => x !== null);
5757

58-
const startNode = MermaidNode.getStartNode();
59-
const finishNode = MermaidNode.getFinishNode();
58+
const startNode = MermaidNode.createStartNode();
59+
const finishNode = MermaidNode.createFinishNode();
6060

6161
const edgesFromStartNode: NullablePartial<GraphEdge>[] = graphNodes
6262
.filter(x => x.predecessors.length === 0)

src/issue-content-parser.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { GitHubIssue, GitHubIssueReference } from "./models";
2-
import { parseIssueUrl } from "./utils";
2+
import { parseIssuesUrls, parseIssueUrl } from "./utils";
33

44
export class IssueContentParser {
55
public extractIssueTasklist(issue: GitHubIssue): GitHubIssueReference[] {
66
const contentLines = issue.body?.split("\n") ?? [];
77

88
return contentLines
9-
.filter(x => x.startsWith("- [ ] "))
9+
.filter(x => this.isTaskListLine(x))
1010
.map(x => parseIssueUrl(x))
1111
.filter((x): x is GitHubIssueReference => x !== null);
1212
}
@@ -15,42 +15,56 @@ export class IssueContentParser {
1515
const contentLines = issue.body?.split("\n") ?? [];
1616

1717
return contentLines
18-
.filter(x => x.startsWith("Depends on"))
19-
.map(x => x.split(",").map(y => parseIssueUrl(y)))
18+
.filter(x => this.isDependencyLine(x))
19+
.map(x => parseIssuesUrls(x))
2020
.flat()
2121
.filter((x): x is GitHubIssueReference => x !== null);
2222
}
2323

2424
public replaceIssueContent(issue: GitHubIssue, sectionTitle: string, newSectionContent: string): string {
25-
const content = issue.body ?? "";
26-
const contentLines = content.split("\n");
25+
const contentLines = issue.body?.split("\n") ?? [];
2726

28-
const sectionStartIndex = contentLines.findIndex(line => this.isLineMarkdownHeader(line, sectionTitle));
27+
const sectionStartIndex = contentLines.findIndex(x => this.isMarkdownHeaderLine(x, sectionTitle));
2928
if (sectionStartIndex === -1) {
30-
throw "";
29+
throw `Markdown header '${sectionTitle}' is not found in issue body:\n ${issue.body}`;
3130
}
3231

3332
const sectionEndIndex = contentLines.findIndex(
34-
(line, lineIndex) => lineIndex > sectionStartIndex && this.isLineMarkdownHeader(line)
33+
(x, index) => index > sectionStartIndex && this.isMarkdownHeaderLine(x)
3534
);
3635

3736
return [
38-
...contentLines.slice(0, sectionStartIndex),
37+
...contentLines.slice(0, sectionStartIndex + 1),
3938
newSectionContent,
40-
...contentLines.slice(sectionEndIndex),
39+
"",
40+
...contentLines.slice(sectionEndIndex !== -1 ? sectionEndIndex : contentLines.length),
4141
].join("\n");
4242
}
4343

44-
private isLineMarkdownHeader(line: string, sectionTitle?: string): boolean {
45-
if (!line.startsWith("#")) {
44+
public isMarkdownHeaderLine(str: string, sectionTitle?: string): boolean {
45+
if (!str.startsWith("#")) {
46+
return false;
47+
}
48+
49+
const trimmedLine = str.replace(/^#+/, "").trim();
50+
if (!trimmedLine) {
4651
return false;
4752
}
4853

4954
if (!sectionTitle) {
5055
return true;
5156
}
5257

53-
const trimmedLine = line.replace(/^#+/, "").trim();
5458
return trimmedLine.toLowerCase() === sectionTitle.toLocaleLowerCase();
5559
}
60+
61+
public isTaskListLine(str: string): boolean {
62+
return str.startsWith("- [ ] ");
63+
}
64+
65+
public isDependencyLine(str: string): boolean {
66+
const dependencyLinePrefixes = ["Dependencies: ", "Predecessors: ", "Depends on ", "Depends on: "];
67+
const formattedLine = str.toLowerCase();
68+
return dependencyLinePrefixes.some(x => formattedLine.startsWith(x.toLowerCase()));
69+
}
5670
}

src/main.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,43 @@ const run = async (): Promise<void> => {
1616
const rootIssue = await githubApiClient.getIssue(config.rootIssue);
1717
const rootIssueTasklist = issueContentParser.extractIssueTasklist(rootIssue);
1818

19+
core.info(`Found ${rootIssueTasklist.length} work items in task list.`);
20+
21+
core.info("Building dependency graph...");
1922
const graphBuilder = new GraphBuilder();
2023
for (const issueRef of rootIssueTasklist) {
2124
const issue = await githubApiClient.getIssue(issueRef);
22-
const issueDetails = MermaidNode.fromGitHubIssue(issue);
23-
graphBuilder.addIssue(issueRef, issueDetails);
24-
25+
const issueDetails = MermaidNode.createFromGitHubIssue(issue);
2526
const issueDependencies = issueContentParser.extractIssueDependencies(issue);
27+
graphBuilder.addIssue(issueRef, issueDetails);
2628
issueDependencies.forEach(x => graphBuilder.addDependency(x, issueRef));
2729
}
2830

2931
const graph = graphBuilder.getGraph();
3032
const renderedContent = mermaidRender.render(graph);
31-
console.log(renderedContent);
33+
34+
core.startGroup("Mermaid diagram");
35+
core.info(renderedContent);
36+
core.endGroup();
3237

3338
const updatedIssueContent = issueContentParser.replaceIssueContent(
3439
rootIssue,
3540
config.sectionTitle,
3641
renderedContent
3742
);
43+
44+
core.startGroup("Updated issue content");
45+
core.info(updatedIssueContent);
46+
core.endGroup();
47+
48+
if (config.dryRun) {
49+
console.log("Action is run in dry-run mode. Root issue won't be updated");
50+
return;
51+
}
52+
53+
core.info("Updating root issue...");
3854
await githubApiClient.updateIssueContent(config.rootIssue, updatedIssueContent);
55+
core.info("Root issue is updated.");
3956
} catch (error) {
4057
if (error instanceof Error) {
4158
core.setFailed(error.message);

0 commit comments

Comments
 (0)