lula2 0.0.1 → 0.0.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 +0 -15
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/crawl.d.ts +59 -0
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +56 -10
- package/package.json +9 -7
- package/src/controls/index.ts +5 -8
- package/src/crawl.ts +144 -80
- package/src/index.ts +10 -10
package/README.md
CHANGED
|
@@ -9,19 +9,4 @@
|
|
|
9
9
|
> OWNER=defenseunicorns REPO=on-demand-compliance PULL_NUMBER=24 GITHUB_TOKEN=asdf npx tsx src/index.ts crawl
|
|
10
10
|
Commenting on file1.ts: **Compliance Alert**: `file1.ts` changed between lines 9–16.
|
|
11
11
|
UUID `123e4567-e89b-12d3-a456-426614174001` may be out of compliance. Please review.
|
|
12
|
-
POST /repos/defenseunicorns/on-demand-compliance/pulls/24/reviews - 422 with id D2F8:C840C:48C6221:933C86B:6890F9BC in 452ms
|
|
13
|
-
Error processing file1.ts: HttpError: Unprocessable Entity: "Can not request changes on your own pull request" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request
|
|
14
|
-
Commenting on file1.yaml: **Compliance Alert**: `file1.yaml` changed between lines 16–18.
|
|
15
|
-
UUID `123e4567-e89b-12d3-a456-426614174000` may be out of compliance. Please review.
|
|
16
|
-
POST /repos/defenseunicorns/on-demand-compliance/pulls/24/reviews - 422 with id D2F8:C840C:48C656D:933CF0C:6890F9BC in 405ms
|
|
17
|
-
Error processing file1.yaml: HttpError: Unprocessable Entity: "Can not request changes on your own pull request" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
┌─[cmwylie19@C2WY6FCQVX] - [~/compliance-cli] - [2025-08-04 02:19:41]
|
|
22
|
-
└─[0] <git:(2 23e3aad) > OWNER=defenseunicorns REPO=on-demand-compliance PULL_NUMBER=24 GITHUB_TOKEN=asdf npx tsx src/index.ts crawl
|
|
23
|
-
Commenting on file1.ts: **Compliance Alert**: `file1.ts` changed between lines 9–16.
|
|
24
|
-
UUID `123e4567-e89b-12d3-a456-426614174001` may be out of compliance. Please review.
|
|
25
|
-
Commenting on file1.yaml: **Compliance Alert**: `file1.yaml` changed between lines 16–18.
|
|
26
|
-
UUID `123e4567-e89b-12d3-a456-426614174000` may be out of compliance. Please review.
|
|
27
12
|
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controls/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controls/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;CAgBnB,CAAC"}
|
package/dist/crawl.d.ts
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
1
2
|
import { Command } from "commander";
|
|
3
|
+
/**
|
|
4
|
+
* Get the pull request context from the environment or GitHub event payload.
|
|
5
|
+
*
|
|
6
|
+
* @returns The pull request context containing the owner, repo, and pull number.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getPRContext(): {
|
|
9
|
+
owner: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
pull_number: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a raw file from a GitHub repository via the GitHub API.
|
|
15
|
+
*
|
|
16
|
+
* @param params - The parameters.
|
|
17
|
+
* @param params.octokit - An authenticated Octokit instance.
|
|
18
|
+
* @param params.owner - The owner of the repository.
|
|
19
|
+
* @param params.repo - The name of the repository.
|
|
20
|
+
* @param params.path - The path to the file in the repository.
|
|
21
|
+
* @param params.ref - The git reference (branch, tag, or commit SHA).
|
|
22
|
+
* @returns The content of the file as a string.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fetchRawFileViaAPI({ octokit, owner, repo, path, ref, }: {
|
|
25
|
+
octokit: Octokit;
|
|
26
|
+
owner: string;
|
|
27
|
+
repo: string;
|
|
28
|
+
path: string;
|
|
29
|
+
ref: string;
|
|
30
|
+
}): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Extracts all @mapStart and @mapEnd blocks from the given content.
|
|
33
|
+
*
|
|
34
|
+
* @param content - The content to extract blocks from.
|
|
35
|
+
*
|
|
36
|
+
* @returns An array of objects containing the UUID, start line, and end line of each block.
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractMapBlocks(content: string): {
|
|
39
|
+
uuid: string;
|
|
40
|
+
startLine: number;
|
|
41
|
+
endLine: number;
|
|
42
|
+
}[];
|
|
43
|
+
/**
|
|
44
|
+
* Get the changed blocks between two versions of text.
|
|
45
|
+
*
|
|
46
|
+
* @param oldText The original text.
|
|
47
|
+
* @param newText The modified text.
|
|
48
|
+
*
|
|
49
|
+
* @returns An array of objects representing the changed blocks.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getChangedBlocks(oldText: string, newText: string): {
|
|
52
|
+
uuid: string;
|
|
53
|
+
startLine: number;
|
|
54
|
+
endLine: number;
|
|
55
|
+
}[];
|
|
56
|
+
/**
|
|
57
|
+
* Defines the "crawl" command for the CLI.
|
|
58
|
+
*
|
|
59
|
+
* @returns The configured Command instance.
|
|
60
|
+
*/
|
|
2
61
|
export default function (): Command;
|
|
3
62
|
//# sourceMappingURL=crawl.d.ts.map
|
package/dist/crawl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crawl.d.ts","sourceRoot":"","sources":["../src/crawl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crawl.d.ts","sourceRoot":"","sources":["../src/crawl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC;;;;GAIG;AACH,wBAAgB,YAAY,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAsBnF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,OAAO,EACP,KAAK,EACL,IAAI,EACJ,IAAI,EACJ,GAAG,GACJ,EAAE;IACD,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,MAAM,CAAC,CA0BlB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,EAAE,CA0BF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd;IACD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,EAAE,CAkBF;AACD;;;;GAIG;AACH,MAAM,CAAC,OAAO,cAAc,OAAO,CA6ClC"}
|
package/dist/crawl.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2025-Present The Lula2 Authors
|
|
1
3
|
import fs from "fs";
|
|
2
4
|
import { Octokit } from "@octokit/rest";
|
|
3
5
|
import { Command } from "commander";
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Get the pull request context from the environment or GitHub event payload.
|
|
8
|
+
*
|
|
9
|
+
* @returns The pull request context containing the owner, repo, and pull number.
|
|
10
|
+
*/
|
|
11
|
+
export function getPRContext() {
|
|
5
12
|
const fallbackOwner = process.env.OWNER;
|
|
6
13
|
const fallbackRepo = process.env.REPO;
|
|
7
14
|
const fallbackNumber = process.env.PULL_NUMBER;
|
|
@@ -22,7 +29,18 @@ function getPRContext() {
|
|
|
22
29
|
pull_number: parseInt(fallbackNumber, 10),
|
|
23
30
|
};
|
|
24
31
|
}
|
|
25
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Fetch a raw file from a GitHub repository via the GitHub API.
|
|
34
|
+
*
|
|
35
|
+
* @param params - The parameters.
|
|
36
|
+
* @param params.octokit - An authenticated Octokit instance.
|
|
37
|
+
* @param params.owner - The owner of the repository.
|
|
38
|
+
* @param params.repo - The name of the repository.
|
|
39
|
+
* @param params.path - The path to the file in the repository.
|
|
40
|
+
* @param params.ref - The git reference (branch, tag, or commit SHA).
|
|
41
|
+
* @returns The content of the file as a string.
|
|
42
|
+
*/
|
|
43
|
+
export async function fetchRawFileViaAPI({ octokit, owner, repo, path, ref, }) {
|
|
26
44
|
const res = await octokit.repos.getContent({
|
|
27
45
|
owner,
|
|
28
46
|
repo,
|
|
@@ -32,11 +50,26 @@ async function fetchRawFileViaAPI(octokit, owner, repo, path, ref) {
|
|
|
32
50
|
accept: "application/vnd.github.v3.raw",
|
|
33
51
|
},
|
|
34
52
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
if (typeof res.data === "string") {
|
|
54
|
+
return res.data;
|
|
55
|
+
}
|
|
56
|
+
if (typeof res.data === "object" &&
|
|
57
|
+
res.data !== null &&
|
|
58
|
+
"content" in res.data &&
|
|
59
|
+
typeof res.data.content === "string") {
|
|
60
|
+
const { content } = res.data;
|
|
61
|
+
return Buffer.from(content, "base64").toString("utf-8");
|
|
62
|
+
}
|
|
63
|
+
throw new Error("Unexpected GitHub API response shape");
|
|
38
64
|
}
|
|
39
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Extracts all @mapStart and @mapEnd blocks from the given content.
|
|
67
|
+
*
|
|
68
|
+
* @param content - The content to extract blocks from.
|
|
69
|
+
*
|
|
70
|
+
* @returns An array of objects containing the UUID, start line, and end line of each block.
|
|
71
|
+
*/
|
|
72
|
+
export function extractMapBlocks(content) {
|
|
40
73
|
const lines = content.split("\n");
|
|
41
74
|
const blocks = [];
|
|
42
75
|
const stack = [];
|
|
@@ -56,7 +89,15 @@ function extractMapBlocks(content) {
|
|
|
56
89
|
});
|
|
57
90
|
return blocks;
|
|
58
91
|
}
|
|
59
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Get the changed blocks between two versions of text.
|
|
94
|
+
*
|
|
95
|
+
* @param oldText The original text.
|
|
96
|
+
* @param newText The modified text.
|
|
97
|
+
*
|
|
98
|
+
* @returns An array of objects representing the changed blocks.
|
|
99
|
+
*/
|
|
100
|
+
export function getChangedBlocks(oldText, newText) {
|
|
60
101
|
const oldBlocks = extractMapBlocks(oldText);
|
|
61
102
|
const newBlocks = extractMapBlocks(newText);
|
|
62
103
|
const changed = [];
|
|
@@ -72,6 +113,11 @@ function getChangedBlocks(oldText, newText) {
|
|
|
72
113
|
}
|
|
73
114
|
return changed;
|
|
74
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Defines the "crawl" command for the CLI.
|
|
118
|
+
*
|
|
119
|
+
* @returns The configured Command instance.
|
|
120
|
+
*/
|
|
75
121
|
export default function () {
|
|
76
122
|
return new Command()
|
|
77
123
|
.command("crawl")
|
|
@@ -87,8 +133,8 @@ export default function () {
|
|
|
87
133
|
continue;
|
|
88
134
|
try {
|
|
89
135
|
const [oldText, newText] = await Promise.all([
|
|
90
|
-
fetchRawFileViaAPI(octokit, owner, repo, file.filename, "main"),
|
|
91
|
-
fetchRawFileViaAPI(octokit, owner, repo, file.filename, prBranch),
|
|
136
|
+
fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: "main" }),
|
|
137
|
+
fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: prBranch }),
|
|
92
138
|
]);
|
|
93
139
|
const changedBlocks = getChangedBlocks(oldText, newText);
|
|
94
140
|
for (const block of changedBlocks) {
|
|
@@ -105,7 +151,7 @@ export default function () {
|
|
|
105
151
|
// repo,
|
|
106
152
|
// pull_number,
|
|
107
153
|
// body: commentBody,
|
|
108
|
-
// event: "REQUEST_CHANGES",
|
|
154
|
+
// event: "REQUEST_CHANGES",
|
|
109
155
|
// })
|
|
110
156
|
}
|
|
111
157
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lula2",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Reports Reports and exports compliance status for defined controls.",
|
|
5
5
|
"bin": "./dist/index.js",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/defenseunicorns/
|
|
14
|
+
"url": "git+https://github.com/defenseunicorns/lula-next.git"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"compliance",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"author": "Defense Unicorns",
|
|
22
22
|
"license": "Apache-2.0",
|
|
23
23
|
"bugs": {
|
|
24
|
-
"url": "https://github.com/defenseunicorns/
|
|
24
|
+
"url": "https://github.com/defenseunicorns/lula-next/issues"
|
|
25
25
|
},
|
|
26
|
-
"homepage": "https://github.com/defenseunicorns/
|
|
26
|
+
"homepage": "https://github.com/defenseunicorns/lula-next#readme",
|
|
27
27
|
"files": [
|
|
28
28
|
"/src",
|
|
29
29
|
"/dist",
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"prebuild": "rm -rf dist",
|
|
36
36
|
"build": "tsc",
|
|
37
37
|
"semantic-release": "semantic-release",
|
|
38
|
-
"format:check": "eslint src
|
|
39
|
-
"format:fix": "eslint --fix src
|
|
40
|
-
"prepare": "if [ \"$NODE_ENV\" != 'production' ]; then husky; fi"
|
|
38
|
+
"format:check": "eslint src && prettier . --check",
|
|
39
|
+
"format:fix": "eslint --fix src && prettier . --write",
|
|
40
|
+
"prepare": "if [ \"$NODE_ENV\" != 'production' ]; then husky; fi",
|
|
41
|
+
"test": "vitest run --coverage"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@octokit/rest": "^22.0.0",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"esbuild": "^0.25.1",
|
|
56
57
|
"eslint": "^9.26.0",
|
|
57
58
|
"eslint-config-prettier": "^10.0.2",
|
|
59
|
+
"eslint-plugin-jsdoc": "^54.1.1",
|
|
58
60
|
"globals": "^16.0.0",
|
|
59
61
|
"husky": "^9.1.7",
|
|
60
62
|
"prettier": "3.5.3",
|
package/src/controls/index.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import { registerControls } from "compliance-reporter"
|
|
1
|
+
import { registerControls } from "compliance-reporter";
|
|
2
2
|
|
|
3
3
|
export const controls = registerControls({
|
|
4
4
|
"123e4567-e89b-12d3-a456-426614174001": {
|
|
5
5
|
id: "AC-1",
|
|
6
6
|
description: "Restrict Pods running as root",
|
|
7
|
-
remarks:
|
|
8
|
-
"This control ensures that no Pods are allowed to run as the root user.",
|
|
7
|
+
remarks: "This control ensures that no Pods are allowed to run as the root user.",
|
|
9
8
|
},
|
|
10
9
|
"123e4567-e89b-12d3-a456-426614174002": {
|
|
11
10
|
id: "AC-2",
|
|
12
11
|
description: "Restrict Pods running privileged containers",
|
|
13
|
-
remarks:
|
|
14
|
-
"This control ensures that no Pods are allowed to run as privileged containers.",
|
|
12
|
+
remarks: "This control ensures that no Pods are allowed to run as privileged containers.",
|
|
15
13
|
},
|
|
16
14
|
"123e4567-e89b-12d3-a456-426614174003": {
|
|
17
15
|
id: "AC-3",
|
|
18
16
|
description: "Restrict Pods running as root",
|
|
19
|
-
remarks:
|
|
20
|
-
"This control ensures that no Pods are allowed to run as the root user.",
|
|
17
|
+
remarks: "This control ensures that no Pods are allowed to run as the root user.",
|
|
21
18
|
},
|
|
22
|
-
})
|
|
19
|
+
});
|
package/src/crawl.ts
CHANGED
|
@@ -1,38 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2025-Present The Lula2 Authors
|
|
3
|
+
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { Octokit } from "@octokit/rest";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
type FileContentResponse = {
|
|
9
|
+
content: string;
|
|
10
|
+
encoding: "base64" | string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the pull request context from the environment or GitHub event payload.
|
|
15
|
+
*
|
|
16
|
+
* @returns The pull request context containing the owner, repo, and pull number.
|
|
17
|
+
*/
|
|
18
|
+
export function getPRContext(): { owner: string; repo: string; pull_number: number } {
|
|
19
|
+
const fallbackOwner = process.env.OWNER;
|
|
20
|
+
const fallbackRepo = process.env.REPO;
|
|
21
|
+
const fallbackNumber = process.env.PULL_NUMBER;
|
|
9
22
|
|
|
10
23
|
if (process.env.GITHUB_EVENT_PATH && process.env.GITHUB_REPOSITORY) {
|
|
11
|
-
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"))
|
|
12
|
-
const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/")
|
|
13
|
-
const pull_number = event.pull_request?.number
|
|
14
|
-
if (!pull_number) throw new Error("PR number not found in GitHub event payload.")
|
|
15
|
-
return { owner, repo, pull_number }
|
|
24
|
+
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
|
25
|
+
const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");
|
|
26
|
+
const pull_number = event.pull_request?.number;
|
|
27
|
+
if (!pull_number) throw new Error("PR number not found in GitHub event payload.");
|
|
28
|
+
return { owner, repo, pull_number };
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
if (!fallbackOwner || !fallbackRepo || !fallbackNumber) {
|
|
19
|
-
throw new Error("Set OWNER, REPO, and PULL_NUMBER in the environment for local use.")
|
|
32
|
+
throw new Error("Set OWNER, REPO, and PULL_NUMBER in the environment for local use.");
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
return {
|
|
23
36
|
owner: fallbackOwner,
|
|
24
37
|
repo: fallbackRepo,
|
|
25
38
|
pull_number: parseInt(fallbackNumber, 10),
|
|
26
|
-
}
|
|
39
|
+
};
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Fetch a raw file from a GitHub repository via the GitHub API.
|
|
44
|
+
*
|
|
45
|
+
* @param params - The parameters.
|
|
46
|
+
* @param params.octokit - An authenticated Octokit instance.
|
|
47
|
+
* @param params.owner - The owner of the repository.
|
|
48
|
+
* @param params.repo - The name of the repository.
|
|
49
|
+
* @param params.path - The path to the file in the repository.
|
|
50
|
+
* @param params.ref - The git reference (branch, tag, or commit SHA).
|
|
51
|
+
* @returns The content of the file as a string.
|
|
52
|
+
*/
|
|
53
|
+
export async function fetchRawFileViaAPI({
|
|
54
|
+
octokit,
|
|
55
|
+
owner,
|
|
56
|
+
repo,
|
|
57
|
+
path,
|
|
58
|
+
ref,
|
|
59
|
+
}: {
|
|
60
|
+
octokit: Octokit;
|
|
61
|
+
owner: string;
|
|
62
|
+
repo: string;
|
|
63
|
+
path: string;
|
|
64
|
+
ref: string;
|
|
65
|
+
}): Promise<string> {
|
|
36
66
|
const res = await octokit.repos.getContent({
|
|
37
67
|
owner,
|
|
38
68
|
repo,
|
|
@@ -41,112 +71,146 @@ async function fetchRawFileViaAPI(
|
|
|
41
71
|
headers: {
|
|
42
72
|
accept: "application/vnd.github.v3.raw",
|
|
43
73
|
},
|
|
44
|
-
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (typeof res.data === "string") {
|
|
77
|
+
return res.data;
|
|
78
|
+
}
|
|
45
79
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
if (
|
|
81
|
+
typeof res.data === "object" &&
|
|
82
|
+
res.data !== null &&
|
|
83
|
+
"content" in res.data &&
|
|
84
|
+
typeof (res.data as { content: unknown }).content === "string"
|
|
85
|
+
) {
|
|
86
|
+
const { content } = res.data as FileContentResponse;
|
|
87
|
+
return Buffer.from(content, "base64").toString("utf-8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error("Unexpected GitHub API response shape");
|
|
49
91
|
}
|
|
50
92
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Extracts all @mapStart and @mapEnd blocks from the given content.
|
|
95
|
+
*
|
|
96
|
+
* @param content - The content to extract blocks from.
|
|
97
|
+
*
|
|
98
|
+
* @returns An array of objects containing the UUID, start line, and end line of each block.
|
|
99
|
+
*/
|
|
100
|
+
export function extractMapBlocks(content: string): {
|
|
101
|
+
uuid: string;
|
|
102
|
+
startLine: number;
|
|
103
|
+
endLine: number;
|
|
55
104
|
}[] {
|
|
56
|
-
const lines = content.split("\n")
|
|
105
|
+
const lines = content.split("\n");
|
|
57
106
|
interface MapBlock {
|
|
58
|
-
uuid: string
|
|
59
|
-
startLine: number
|
|
60
|
-
endLine: number
|
|
107
|
+
uuid: string;
|
|
108
|
+
startLine: number;
|
|
109
|
+
endLine: number;
|
|
61
110
|
}
|
|
62
|
-
const blocks: MapBlock[] = []
|
|
63
|
-
const stack: { uuid: string; line: number }[] = []
|
|
111
|
+
const blocks: MapBlock[] = [];
|
|
112
|
+
const stack: { uuid: string; line: number }[] = [];
|
|
64
113
|
|
|
65
114
|
lines.forEach((line, idx) => {
|
|
66
|
-
const start = line.match(/@mapStart\s+([a-f0-9-]+)/)
|
|
67
|
-
const end = line.match(/@mapEnd\s+([a-f0-9-]+)/)
|
|
115
|
+
const start = line.match(/@mapStart\s+([a-f0-9-]+)/);
|
|
116
|
+
const end = line.match(/@mapEnd\s+([a-f0-9-]+)/);
|
|
68
117
|
|
|
69
118
|
if (start) {
|
|
70
|
-
stack.push({ uuid: start[1], line: idx })
|
|
119
|
+
stack.push({ uuid: start[1], line: idx });
|
|
71
120
|
} else if (end) {
|
|
72
|
-
const last = stack.find(s => s.uuid === end[1])
|
|
121
|
+
const last = stack.find(s => s.uuid === end[1]);
|
|
73
122
|
if (last) {
|
|
74
|
-
blocks.push({ uuid: last.uuid, startLine: last.line, endLine: idx + 1 })
|
|
75
|
-
stack.splice(stack.indexOf(last), 1)
|
|
123
|
+
blocks.push({ uuid: last.uuid, startLine: last.line, endLine: idx + 1 });
|
|
124
|
+
stack.splice(stack.indexOf(last), 1);
|
|
76
125
|
}
|
|
77
126
|
}
|
|
78
|
-
})
|
|
127
|
+
});
|
|
79
128
|
|
|
80
|
-
return blocks
|
|
129
|
+
return blocks;
|
|
81
130
|
}
|
|
82
131
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Get the changed blocks between two versions of text.
|
|
134
|
+
*
|
|
135
|
+
* @param oldText The original text.
|
|
136
|
+
* @param newText The modified text.
|
|
137
|
+
*
|
|
138
|
+
* @returns An array of objects representing the changed blocks.
|
|
139
|
+
*/
|
|
140
|
+
export function getChangedBlocks(
|
|
141
|
+
oldText: string,
|
|
142
|
+
newText: string,
|
|
143
|
+
): {
|
|
144
|
+
uuid: string;
|
|
145
|
+
startLine: number;
|
|
146
|
+
endLine: number;
|
|
87
147
|
}[] {
|
|
88
|
-
const oldBlocks = extractMapBlocks(oldText)
|
|
89
|
-
const newBlocks = extractMapBlocks(newText)
|
|
90
|
-
const changed = []
|
|
148
|
+
const oldBlocks = extractMapBlocks(oldText);
|
|
149
|
+
const newBlocks = extractMapBlocks(newText);
|
|
150
|
+
const changed = [];
|
|
91
151
|
|
|
92
152
|
for (const newBlock of newBlocks) {
|
|
93
|
-
const oldMatch = oldBlocks.find(b => b.uuid === newBlock.uuid)
|
|
94
|
-
if (!oldMatch) continue
|
|
153
|
+
const oldMatch = oldBlocks.find(b => b.uuid === newBlock.uuid);
|
|
154
|
+
if (!oldMatch) continue;
|
|
95
155
|
|
|
96
|
-
const oldSegment = oldText.split("\n").slice(oldMatch.startLine, oldMatch.endLine).join("\n")
|
|
97
|
-
const newSegment = newText.split("\n").slice(newBlock.startLine, newBlock.endLine).join("\n")
|
|
156
|
+
const oldSegment = oldText.split("\n").slice(oldMatch.startLine, oldMatch.endLine).join("\n");
|
|
157
|
+
const newSegment = newText.split("\n").slice(newBlock.startLine, newBlock.endLine).join("\n");
|
|
98
158
|
|
|
99
159
|
if (oldSegment !== newSegment) {
|
|
100
|
-
changed.push(newBlock)
|
|
160
|
+
changed.push(newBlock);
|
|
101
161
|
}
|
|
102
162
|
}
|
|
103
163
|
|
|
104
|
-
return changed
|
|
164
|
+
return changed;
|
|
105
165
|
}
|
|
106
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Defines the "crawl" command for the CLI.
|
|
168
|
+
*
|
|
169
|
+
* @returns The configured Command instance.
|
|
170
|
+
*/
|
|
107
171
|
export default function (): Command {
|
|
108
172
|
return new Command()
|
|
109
173
|
.command("crawl")
|
|
110
174
|
.description("Detect compliance-related changes between @mapStart and @mapEnd in PR files")
|
|
111
175
|
.action(async () => {
|
|
112
|
-
const { owner, repo, pull_number } = getPRContext()
|
|
113
|
-
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
|
176
|
+
const { owner, repo, pull_number } = getPRContext();
|
|
177
|
+
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
114
178
|
|
|
115
|
-
const pr = await octokit.pulls.get({ owner, repo, pull_number })
|
|
116
|
-
const prBranch = pr.data.head.ref
|
|
179
|
+
const pr = await octokit.pulls.get({ owner, repo, pull_number });
|
|
180
|
+
const prBranch = pr.data.head.ref;
|
|
117
181
|
|
|
118
|
-
const { data: files } = await octokit.pulls.listFiles({ owner, repo, pull_number })
|
|
182
|
+
const { data: files } = await octokit.pulls.listFiles({ owner, repo, pull_number });
|
|
119
183
|
|
|
120
184
|
for (const file of files) {
|
|
121
|
-
if (file.status === "added") continue
|
|
185
|
+
if (file.status === "added") continue;
|
|
122
186
|
try {
|
|
123
187
|
const [oldText, newText] = await Promise.all([
|
|
124
|
-
fetchRawFileViaAPI(octokit, owner, repo, file.filename, "main"),
|
|
125
|
-
fetchRawFileViaAPI(octokit, owner, repo, file.filename, prBranch),
|
|
126
|
-
])
|
|
188
|
+
fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: "main" }),
|
|
189
|
+
fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: prBranch }),
|
|
190
|
+
]);
|
|
127
191
|
|
|
128
|
-
const changedBlocks = getChangedBlocks(oldText, newText)
|
|
192
|
+
const changedBlocks = getChangedBlocks(oldText, newText);
|
|
129
193
|
|
|
130
194
|
for (const block of changedBlocks) {
|
|
131
|
-
const commentBody = `**Compliance Alert**: \`${file.filename}\` changed between lines ${block.startLine + 1}–${block.endLine}.\nUUID \`${block.uuid}\` may be out of compliance. Please review
|
|
132
|
-
console.log(`Commenting on ${file.filename}: ${commentBody}`)
|
|
195
|
+
const commentBody = `**Compliance Alert**: \`${file.filename}\` changed between lines ${block.startLine + 1}–${block.endLine}.\nUUID \`${block.uuid}\` may be out of compliance. Please review.`;
|
|
196
|
+
console.log(`Commenting on ${file.filename}: ${commentBody}`);
|
|
133
197
|
await octokit.issues.createComment({
|
|
134
198
|
owner,
|
|
135
199
|
repo,
|
|
136
200
|
issue_number: pull_number,
|
|
137
201
|
body: commentBody,
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
202
|
+
});
|
|
203
|
+
// await octokit.pulls.createReview({
|
|
204
|
+
// owner,
|
|
205
|
+
// repo,
|
|
206
|
+
// pull_number,
|
|
207
|
+
// body: commentBody,
|
|
208
|
+
// event: "REQUEST_CHANGES",
|
|
209
|
+
// })
|
|
146
210
|
}
|
|
147
211
|
} catch (err) {
|
|
148
|
-
console.error(`Error processing ${file.filename}: ${err}`)
|
|
212
|
+
console.error(`Error processing ${file.filename}: ${err}`);
|
|
149
213
|
}
|
|
150
214
|
}
|
|
151
|
-
})
|
|
215
|
+
});
|
|
152
216
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { Command } from "commander"
|
|
4
|
-
import crawl from "./crawl.js"
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import crawl from "./crawl.js";
|
|
5
5
|
// Define the program
|
|
6
|
-
const program = new Command()
|
|
6
|
+
const program = new Command();
|
|
7
7
|
|
|
8
8
|
// Set basic information
|
|
9
9
|
program
|
|
@@ -12,18 +12,18 @@ program
|
|
|
12
12
|
.option("-c, --config <path>", "path to config file", "compliance.json")
|
|
13
13
|
.addCommand(crawl())
|
|
14
14
|
.action(options => {
|
|
15
|
-
console.log("Checking compliance status...")
|
|
15
|
+
console.log("Checking compliance status...");
|
|
16
16
|
if (options.config) {
|
|
17
|
-
console.log(`Using config file: ${options.config}`)
|
|
17
|
+
console.log(`Using config file: ${options.config}`);
|
|
18
18
|
} else {
|
|
19
|
-
console.log("Using default configuration")
|
|
19
|
+
console.log("Using default configuration");
|
|
20
20
|
}
|
|
21
|
-
console.log("Compliance check completed!")
|
|
22
|
-
})
|
|
21
|
+
console.log("Compliance check completed!");
|
|
22
|
+
});
|
|
23
23
|
|
|
24
|
-
program.parse(process.argv)
|
|
24
|
+
program.parse(process.argv);
|
|
25
25
|
|
|
26
26
|
// If no command is provided, show help
|
|
27
27
|
if (!process.argv.slice(1).length) {
|
|
28
|
-
program.help()
|
|
28
|
+
program.help();
|
|
29
29
|
}
|