@oneuptime/common 11.5.7 → 11.5.9
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/Models/DatabaseModels/AIAgentTaskPullRequest.ts +15 -6
- package/Models/DatabaseModels/AIRun.ts +68 -0
- package/Models/DatabaseModels/AIRunEvent.ts +34 -1
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +215 -0
- package/Models/DatabaseModels/Project.ts +25 -0
- package/Models/DatabaseModels/StatusPage.ts +42 -0
- package/Server/API/AIAgentTaskAPI.ts +38 -12
- package/Server/API/AIAgentTaskLogAPI.ts +49 -0
- package/Server/API/AIChatAPI.ts +16 -0
- package/Server/API/AIReadinessAPI.ts +84 -0
- package/Server/API/CodeFixRunAPI.ts +190 -57
- package/Server/API/TelemetryExceptionAPI.ts +6 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.ts +95 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +1 -1
- package/Server/Middleware/ProjectAuthorization.ts +1 -1
- package/Server/Services/AIRunEventService.ts +15 -1
- package/Server/Services/AIRunService.ts +48 -12
- package/Server/Services/LlmProviderService.ts +2 -0
- package/Server/Services/ProjectService.ts +42 -0
- package/Server/Services/StatusPageService.ts +87 -0
- package/Server/Services/TelemetryExceptionService.ts +19 -90
- package/Server/Types/Markdown.ts +9 -9
- package/Server/Types/MarkdownSlugify.ts +47 -0
- package/Server/Utils/AI/AIRunPrivacyFilter.ts +106 -0
- package/Server/Utils/AI/AIRunTranscript.ts +240 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +119 -5
- package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +75 -0
- package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +235 -0
- package/Server/Utils/AI/SRE/Insights/FixRouting.ts +2 -3
- package/Server/Utils/AI/Toolbox/CodeTools.ts +883 -0
- package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +519 -0
- package/Server/Utils/AI/Toolbox/Index.ts +27 -0
- package/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.ts +279 -0
- package/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.ts +19 -1
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +600 -0
- package/Server/Utils/CodeRepository/StackTraceRepoResolver.ts +7 -2
- package/Server/Utils/LLM/LLMService.ts +397 -18
- package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +4 -11
- package/Tests/Models/StatusPageEnableMcpServer.test.ts +125 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +223 -0
- package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +26 -3
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +12 -0
- package/Tests/Server/Types/Markdown.test.ts +106 -0
- package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +295 -0
- package/Tests/Server/Utils/AI/AIRunTranscript.test.ts +182 -0
- package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +411 -0
- package/Tests/Server/Utils/AI/CodeTools.test.ts +596 -0
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +475 -0
- package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +548 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +142 -0
- package/Tests/Server/Utils/AI/ScheduledMaintenanceTools.test.ts +176 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.test.ts +63 -0
- package/Tests/Server/Utils/GitHubGetFileContent.test.ts +197 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +502 -0
- package/Tests/Types/AI/AIAgentTaskStatus.test.ts +7 -6
- package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
- package/Tests/Types/AI/AIRunStatus.test.ts +42 -0
- package/Tests/Types/Log/LogSeverity.test.ts +78 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +167 -0
- package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +66 -0
- package/Tests/UI/Utils/AIChatExport/ChatWidgetData.test.ts +319 -0
- package/Tests/UI/Utils/AIChatExport/ChatWidgetMarkdown.test.ts +188 -0
- package/Types/AI/AIAgentTaskStatus.ts +13 -0
- package/Types/AI/AIChatPageContext.ts +137 -0
- package/Types/AI/AIChatTypes.ts +44 -0
- package/Types/AI/AIFixReadiness.ts +46 -0
- package/Types/AI/AIRunStatus.ts +11 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Log/LogSeverity.ts +61 -0
- package/Types/Monitor/SnmpMonitor/SnmpVersion.ts +40 -0
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -2
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +3 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +8 -2
- package/UI/Components/EditionLabel/EditionLabel.tsx +821 -331
- package/UI/Components/Forms/BasicForm.tsx +26 -8
- package/UI/Styles/Theme.css +1314 -0
- package/UI/Utils/AIChatExport/ChatWidgetData.ts +316 -0
- package/UI/Utils/AIChatExport/ChatWidgetMarkdown.ts +349 -0
- package/UI/Utils/AIChatExport/ConversationMarkdown.ts +177 -0
- package/UI/Utils/AIChatExport/MarkdownBlocks.ts +327 -0
- package/UI/Utils/AIChatExport/MarkdownSafety.ts +215 -0
- package/UI/Utils/DownloadFile.ts +54 -0
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +15 -7
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRun.js +69 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +31 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +225 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +27 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +43 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskAPI.js +37 -11
- package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskLogAPI.js +32 -7
- package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +9 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIReadinessAPI.js +57 -0
- package/build/dist/Server/API/AIReadinessAPI.js.map +1 -0
- package/build/dist/Server/API/CodeFixRunAPI.js +165 -51
- package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryExceptionAPI.js +4 -0
- package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js +80 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +1 -1
- package/build/dist/Server/Services/AIRunEventService.js +8 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +39 -3
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +2 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +39 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +81 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +14 -61
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +8 -8
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/MarkdownSlugify.js +47 -0
- package/build/dist/Server/Types/MarkdownSlugify.js.map +1 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +82 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/AIRunTranscript.js +180 -0
- package/build/dist/Server/Utils/AI/AIRunTranscript.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +103 -5
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +55 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +211 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +654 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +389 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +19 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js +239 -0
- package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js +19 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +439 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js +1 -1
- package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +275 -19
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +2 -11
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
- package/build/dist/Types/AI/AIAgentTaskStatus.js +12 -0
- package/build/dist/Types/AI/AIAgentTaskStatus.js.map +1 -1
- package/build/dist/Types/AI/AIChatPageContext.js +101 -0
- package/build/dist/Types/AI/AIChatPageContext.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +2 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/AI/AIFixReadiness.js +9 -0
- package/build/dist/Types/AI/AIFixReadiness.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +11 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Log/LogSeverity.js +56 -0
- package/build/dist/Types/Log/LogSeverity.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js +38 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +9 -2
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +3 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +9 -2
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +424 -139
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +24 -6
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js +217 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js +234 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +111 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js +233 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js +134 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js.map +1 -0
- package/build/dist/UI/Utils/DownloadFile.js +41 -0
- package/build/dist/UI/Utils/DownloadFile.js.map +1 -0
- package/package.json +5 -1
|
@@ -4,6 +4,7 @@ import HostedCodeRepository from "../HostedCodeRepository/HostedCodeRepository";
|
|
|
4
4
|
import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
|
|
5
5
|
import HTTPResponse from "../../../../Types/API/HTTPResponse";
|
|
6
6
|
import URL from "../../../../Types/API/URL";
|
|
7
|
+
import Headers from "../../../../Types/API/Headers";
|
|
7
8
|
import PullRequest from "../../../../Types/CodeRepository/PullRequest";
|
|
8
9
|
import PullRequestState from "../../../../Types/CodeRepository/PullRequestState";
|
|
9
10
|
import FixPullRequestCiStatus, {
|
|
@@ -55,6 +56,34 @@ export interface GitHubCheckRunsSummary extends FixPullRequestCiCheckRunCounts {
|
|
|
55
56
|
conclusion: FixPullRequestCiStatus;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// One file's contents read over the Contents API (no clone).
|
|
60
|
+
export interface GitHubFileContent {
|
|
61
|
+
content: string;
|
|
62
|
+
filePath: string;
|
|
63
|
+
sizeInBytes: number;
|
|
64
|
+
totalLines: number;
|
|
65
|
+
htmlUrl: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// One branch's head, plus whether GitHub considers it protected.
|
|
69
|
+
export interface GitHubBranchInfo {
|
|
70
|
+
name: string;
|
|
71
|
+
headSha: string;
|
|
72
|
+
isProtected: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A file to write in a commit. content is the FULL new file body.
|
|
76
|
+
export interface GitHubFileChange {
|
|
77
|
+
filePath: string;
|
|
78
|
+
content: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface GitHubCommitResult {
|
|
82
|
+
commitSha: string;
|
|
83
|
+
branchName: string;
|
|
84
|
+
htmlUrl: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
export default class GitHubUtil extends HostedCodeRepository {
|
|
59
88
|
private getPullRequestFromJSONObject(data: {
|
|
60
89
|
pullRequest: JSONObject;
|
|
@@ -805,6 +834,577 @@ export default class GitHubUtil extends HostedCodeRepository {
|
|
|
805
834
|
return paths;
|
|
806
835
|
}
|
|
807
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Reads one file's contents from a repository over the Contents API, with no
|
|
839
|
+
* clone. This is what makes reading code viable from a request-scoped caller
|
|
840
|
+
* (e.g. the chat toolbox, which has a 45s per-tool timeout a clone cannot
|
|
841
|
+
* fit).
|
|
842
|
+
*
|
|
843
|
+
* Returns null when the path does not exist on the branch, so callers can
|
|
844
|
+
* tell "no such file" apart from a transport failure without catching.
|
|
845
|
+
* @returns The file's text plus its total line count, or null if not found
|
|
846
|
+
*/
|
|
847
|
+
@CaptureSpan()
|
|
848
|
+
public static async getFileContent(data: {
|
|
849
|
+
installationId: string;
|
|
850
|
+
organizationName: string;
|
|
851
|
+
repositoryName: string;
|
|
852
|
+
branchName: string;
|
|
853
|
+
filePath: string;
|
|
854
|
+
}): Promise<GitHubFileContent | null> {
|
|
855
|
+
/*
|
|
856
|
+
* Each path segment is encoded separately: encodeURIComponent would turn
|
|
857
|
+
* the directory separators into %2F, which the Contents API treats as part
|
|
858
|
+
* of a (nonexistent) filename rather than as a path.
|
|
859
|
+
*/
|
|
860
|
+
const encodedPath: string = data.filePath
|
|
861
|
+
.split("/")
|
|
862
|
+
.map((segment: string) => {
|
|
863
|
+
return encodeURIComponent(segment);
|
|
864
|
+
})
|
|
865
|
+
.join("/");
|
|
866
|
+
|
|
867
|
+
const url: URL = URL.fromString(
|
|
868
|
+
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/contents/${encodedPath}?ref=${encodeURIComponent(data.branchName)}`,
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const tokenData: GitHubInstallationToken =
|
|
872
|
+
await GitHubUtil.getInstallationAccessToken(data.installationId, {
|
|
873
|
+
permissions: {
|
|
874
|
+
contents: "read",
|
|
875
|
+
metadata: "read",
|
|
876
|
+
},
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.get({
|
|
880
|
+
url: url,
|
|
881
|
+
headers: {
|
|
882
|
+
Authorization: `Bearer ${tokenData.token}`,
|
|
883
|
+
Accept: "application/vnd.github+json",
|
|
884
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
if (result instanceof HTTPErrorResponse) {
|
|
889
|
+
if (result.statusCode === 404) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
throw result;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/*
|
|
896
|
+
* A directory returns a JSON array, not an object. Treat it as "not a
|
|
897
|
+
* readable file" rather than erroring — the model routinely guesses a
|
|
898
|
+
* directory path, and a null lets it self-correct via search_code.
|
|
899
|
+
*/
|
|
900
|
+
if (Array.isArray(result.data)) {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (result.data["type"] !== "file") {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const sizeInBytes: number = (result.data["size"] as number) || 0;
|
|
909
|
+
const encodedContent: string = (result.data["content"] as string) || "";
|
|
910
|
+
|
|
911
|
+
/*
|
|
912
|
+
* `encoding` — not an empty `content` — is what distinguishes an oversized
|
|
913
|
+
* blob from an empty one. Over ~1MB the API declines to inline the blob and
|
|
914
|
+
* returns encoding "none" with content ""; a legitimately empty 0-byte file
|
|
915
|
+
* returns encoding "base64" with content "" too. Keying off the empty
|
|
916
|
+
* string alone would report every empty file as too large.
|
|
917
|
+
*/
|
|
918
|
+
const encoding: string = (result.data["encoding"] as string) || "";
|
|
919
|
+
|
|
920
|
+
if (encoding === "none") {
|
|
921
|
+
throw new BadDataException(
|
|
922
|
+
`${data.filePath} is ${sizeInBytes} bytes, which is too large for GitHub to return inline (the limit is 1MB). Read a specific region of it is not possible via this tool.`,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (encoding && encoding !== "base64") {
|
|
927
|
+
throw new BadDataException(
|
|
928
|
+
`${data.filePath} came back in an unsupported encoding ("${encoding}").`,
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const content: string = encodedContent
|
|
933
|
+
? Buffer.from(encodedContent.replace(/\n/g, ""), "base64").toString(
|
|
934
|
+
"utf8",
|
|
935
|
+
)
|
|
936
|
+
: "";
|
|
937
|
+
|
|
938
|
+
/*
|
|
939
|
+
* Binary files decode into mojibake that wastes context and can never
|
|
940
|
+
* answer a question. A NUL byte is the cheapest reliable discriminator.
|
|
941
|
+
*/
|
|
942
|
+
if (content.includes("\0")) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
content: content,
|
|
948
|
+
filePath: data.filePath,
|
|
949
|
+
sizeInBytes: sizeInBytes,
|
|
950
|
+
totalLines: GitHubUtil.countLines(content),
|
|
951
|
+
htmlUrl: (result.data["html_url"] as string) || "",
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/*
|
|
956
|
+
* Line count that does not invent a phantom blank line for the trailing
|
|
957
|
+
* newline virtually every source file ends with (a plain split("\n") counts
|
|
958
|
+
* the empty string after it).
|
|
959
|
+
*/
|
|
960
|
+
private static countLines(content: string): number {
|
|
961
|
+
if (content === "") {
|
|
962
|
+
return 0;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const lines: number = content.split("\n").length;
|
|
966
|
+
|
|
967
|
+
return content.endsWith("\n") ? lines - 1 : lines;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/*
|
|
971
|
+
* The repository's real default branch, read from GitHub rather than from
|
|
972
|
+
* our stored mainBranchName. Callers refusing to write to the default branch
|
|
973
|
+
* must ask GitHub: our column is a copy made at import time and drifts when
|
|
974
|
+
* the repository is renamed or its default is changed.
|
|
975
|
+
*/
|
|
976
|
+
@CaptureSpan()
|
|
977
|
+
public static async getDefaultBranchName(data: {
|
|
978
|
+
installationId: string;
|
|
979
|
+
organizationName: string;
|
|
980
|
+
repositoryName: string;
|
|
981
|
+
}): Promise<string> {
|
|
982
|
+
const tokenData: GitHubInstallationToken =
|
|
983
|
+
await GitHubUtil.getInstallationAccessToken(data.installationId, {
|
|
984
|
+
permissions: { metadata: "read" },
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.get({
|
|
988
|
+
url: URL.fromString(
|
|
989
|
+
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}`,
|
|
990
|
+
),
|
|
991
|
+
headers: GitHubUtil.buildApiHeaders(tokenData.token),
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
if (result instanceof HTTPErrorResponse) {
|
|
995
|
+
throw result;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return (result.data["default_branch"] as string) || "";
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/*
|
|
1002
|
+
* One branch's head sha and protection flag, or null when the branch does
|
|
1003
|
+
* not exist. Protection is part of the same payload, so a caller enforcing
|
|
1004
|
+
* "never write to a protected branch" needs no second round-trip.
|
|
1005
|
+
*/
|
|
1006
|
+
@CaptureSpan()
|
|
1007
|
+
public static async getBranch(data: {
|
|
1008
|
+
installationId: string;
|
|
1009
|
+
organizationName: string;
|
|
1010
|
+
repositoryName: string;
|
|
1011
|
+
branchName: string;
|
|
1012
|
+
}): Promise<GitHubBranchInfo | null> {
|
|
1013
|
+
const tokenData: GitHubInstallationToken =
|
|
1014
|
+
await GitHubUtil.getInstallationAccessToken(data.installationId, {
|
|
1015
|
+
permissions: { contents: "read", metadata: "read" },
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.get({
|
|
1019
|
+
url: URL.fromString(
|
|
1020
|
+
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/branches/${encodeURIComponent(data.branchName)}`,
|
|
1021
|
+
),
|
|
1022
|
+
headers: GitHubUtil.buildApiHeaders(tokenData.token),
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
if (result instanceof HTTPErrorResponse) {
|
|
1026
|
+
if (result.statusCode === 404) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
throw result;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const commit: JSONObject = (result.data["commit"] as JSONObject) || {};
|
|
1033
|
+
|
|
1034
|
+
return {
|
|
1035
|
+
name: (result.data["name"] as string) || data.branchName,
|
|
1036
|
+
headSha: (commit["sha"] as string) || "",
|
|
1037
|
+
isProtected: result.data["protected"] === true,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/*
|
|
1042
|
+
* Commit a set of whole-file changes onto an EXISTING branch, with no clone,
|
|
1043
|
+
* via the Git Data API (blobs → tree → commit → move ref).
|
|
1044
|
+
*
|
|
1045
|
+
* The tree is built on the branch head's tree, so files not named here are
|
|
1046
|
+
* untouched, and all changes land as ONE commit — a partial write cannot be
|
|
1047
|
+
* left behind if a later step fails.
|
|
1048
|
+
*
|
|
1049
|
+
* This deliberately does NOT decide what may be written to: enforcing the
|
|
1050
|
+
* default/protected-branch refusal is the caller's job, because only the
|
|
1051
|
+
* caller knows the trust context of the change.
|
|
1052
|
+
*/
|
|
1053
|
+
@CaptureSpan()
|
|
1054
|
+
public static async commitFilesToBranch(data: {
|
|
1055
|
+
installationId: string;
|
|
1056
|
+
organizationName: string;
|
|
1057
|
+
repositoryName: string;
|
|
1058
|
+
branchName: string;
|
|
1059
|
+
changes: Array<GitHubFileChange>;
|
|
1060
|
+
commitMessage: string;
|
|
1061
|
+
}): Promise<GitHubCommitResult> {
|
|
1062
|
+
if (data.changes.length === 0) {
|
|
1063
|
+
throw new BadDataException("No file changes were provided to commit.");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const tokenData: GitHubInstallationToken =
|
|
1067
|
+
await GitHubUtil.getInstallationAccessToken(data.installationId, {
|
|
1068
|
+
permissions: { contents: "write", metadata: "read" },
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const headers: Headers = GitHubUtil.buildApiHeaders(tokenData.token);
|
|
1072
|
+
const repoApiUrl: string = `https://api.github.com/repos/${data.organizationName}/${data.repositoryName}`;
|
|
1073
|
+
|
|
1074
|
+
// 1. Where the branch currently points.
|
|
1075
|
+
const refResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
1076
|
+
await API.get({
|
|
1077
|
+
url: URL.fromString(
|
|
1078
|
+
`${repoApiUrl}/git/ref/heads/${encodeURIComponent(data.branchName)}`,
|
|
1079
|
+
),
|
|
1080
|
+
headers: headers,
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
if (refResult instanceof HTTPErrorResponse) {
|
|
1084
|
+
throw refResult;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const baseCommitSha: string = ((refResult.data["object"] as JSONObject) ||
|
|
1088
|
+
{})["sha"] as string;
|
|
1089
|
+
|
|
1090
|
+
// 2. That commit's tree, so unnamed files carry over untouched.
|
|
1091
|
+
const baseCommitResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
1092
|
+
await API.get({
|
|
1093
|
+
url: URL.fromString(`${repoApiUrl}/git/commits/${baseCommitSha}`),
|
|
1094
|
+
headers: headers,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
if (baseCommitResult instanceof HTTPErrorResponse) {
|
|
1098
|
+
throw baseCommitResult;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const baseTreeSha: string = ((baseCommitResult.data[
|
|
1102
|
+
"tree"
|
|
1103
|
+
] as JSONObject) || {})["sha"] as string;
|
|
1104
|
+
|
|
1105
|
+
/*
|
|
1106
|
+
* 3. The existing mode of every path we are about to rewrite.
|
|
1107
|
+
*
|
|
1108
|
+
* A tree entry is (mode, type, path, sha) and an entry we supply REPLACES
|
|
1109
|
+
* the base_tree's entry wholesale — there is no "keep the existing mode"
|
|
1110
|
+
* option. Hardcoding 100644 would therefore silently demote executables
|
|
1111
|
+
* (100755 -> not executable) and turn symlinks (120000) into regular files
|
|
1112
|
+
* holding the link text. Both render as a one-line mode header in a diff
|
|
1113
|
+
* that reviewers skim past, so it must be right here rather than caught in
|
|
1114
|
+
* review.
|
|
1115
|
+
*/
|
|
1116
|
+
const existingModes: Map<string, string> =
|
|
1117
|
+
await GitHubUtil.getTreeEntryModes({
|
|
1118
|
+
repoApiUrl,
|
|
1119
|
+
headers,
|
|
1120
|
+
treeSha: baseTreeSha,
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// 4. A blob per changed file, carrying the path's existing mode forward.
|
|
1124
|
+
const treeEntries: JSONArray = [];
|
|
1125
|
+
|
|
1126
|
+
for (const change of data.changes) {
|
|
1127
|
+
const blobResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
1128
|
+
await API.post({
|
|
1129
|
+
url: URL.fromString(`${repoApiUrl}/git/blobs`),
|
|
1130
|
+
data: {
|
|
1131
|
+
content: Buffer.from(change.content, "utf8").toString("base64"),
|
|
1132
|
+
encoding: "base64",
|
|
1133
|
+
},
|
|
1134
|
+
headers: headers,
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
if (blobResult instanceof HTTPErrorResponse) {
|
|
1138
|
+
throw blobResult;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const existingMode: string | undefined = existingModes.get(
|
|
1142
|
+
change.filePath,
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
/*
|
|
1146
|
+
* A symlink or submodule is not something a whole-file text write can
|
|
1147
|
+
* safely replace: writing a blob at a 120000 path would silently turn the
|
|
1148
|
+
* link into a file. Refuse rather than corrupt the tree.
|
|
1149
|
+
*/
|
|
1150
|
+
if (existingMode === "120000" || existingMode === "160000") {
|
|
1151
|
+
throw new BadDataException(
|
|
1152
|
+
`"${change.filePath}" is a ${existingMode === "120000" ? "symlink" : "submodule"}, which cannot be rewritten as a text file.`,
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
treeEntries.push({
|
|
1157
|
+
path: change.filePath,
|
|
1158
|
+
// New files default to a regular non-executable file.
|
|
1159
|
+
mode: existingMode || "100644",
|
|
1160
|
+
type: "blob",
|
|
1161
|
+
sha: blobResult.data["sha"] as string,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// 5. A tree layering those blobs over the base tree.
|
|
1166
|
+
const treeResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
1167
|
+
await API.post({
|
|
1168
|
+
url: URL.fromString(`${repoApiUrl}/git/trees`),
|
|
1169
|
+
data: {
|
|
1170
|
+
base_tree: baseTreeSha,
|
|
1171
|
+
tree: treeEntries,
|
|
1172
|
+
},
|
|
1173
|
+
headers: headers,
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
if (treeResult instanceof HTTPErrorResponse) {
|
|
1177
|
+
throw treeResult;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// 6. The commit itself.
|
|
1181
|
+
const commitResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
1182
|
+
await API.post({
|
|
1183
|
+
url: URL.fromString(`${repoApiUrl}/git/commits`),
|
|
1184
|
+
data: {
|
|
1185
|
+
message: data.commitMessage,
|
|
1186
|
+
tree: treeResult.data["sha"] as string,
|
|
1187
|
+
parents: [baseCommitSha],
|
|
1188
|
+
},
|
|
1189
|
+
headers: headers,
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
if (commitResult instanceof HTTPErrorResponse) {
|
|
1193
|
+
throw commitResult;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const newCommitSha: string = commitResult.data["sha"] as string;
|
|
1197
|
+
|
|
1198
|
+
/*
|
|
1199
|
+
* 7. Move the branch. Not forced: a fast-forward-only update means a
|
|
1200
|
+
* concurrent push to this branch makes us fail loudly rather than silently
|
|
1201
|
+
* discarding someone else's commit.
|
|
1202
|
+
*/
|
|
1203
|
+
const updateResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
1204
|
+
await API.patch({
|
|
1205
|
+
url: URL.fromString(
|
|
1206
|
+
`${repoApiUrl}/git/refs/heads/${encodeURIComponent(data.branchName)}`,
|
|
1207
|
+
),
|
|
1208
|
+
data: {
|
|
1209
|
+
sha: newCommitSha,
|
|
1210
|
+
force: false,
|
|
1211
|
+
},
|
|
1212
|
+
headers: headers,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
if (updateResult instanceof HTTPErrorResponse) {
|
|
1216
|
+
throw updateResult;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return {
|
|
1220
|
+
commitSha: newCommitSha,
|
|
1221
|
+
branchName: data.branchName,
|
|
1222
|
+
htmlUrl: `https://github.com/${data.organizationName}/${data.repositoryName}/commit/${newCommitSha}`,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/*
|
|
1227
|
+
* Create a branch pointing at another branch's head. Returns false when the
|
|
1228
|
+
* branch already exists (GitHub answers 422), so callers can pick a fresh
|
|
1229
|
+
* name rather than treating it as a hard failure.
|
|
1230
|
+
*/
|
|
1231
|
+
@CaptureSpan()
|
|
1232
|
+
public static async createBranch(data: {
|
|
1233
|
+
installationId: string;
|
|
1234
|
+
organizationName: string;
|
|
1235
|
+
repositoryName: string;
|
|
1236
|
+
newBranchName: string;
|
|
1237
|
+
fromBranchName: string;
|
|
1238
|
+
}): Promise<boolean> {
|
|
1239
|
+
const source: GitHubBranchInfo | null = await GitHubUtil.getBranch({
|
|
1240
|
+
installationId: data.installationId,
|
|
1241
|
+
organizationName: data.organizationName,
|
|
1242
|
+
repositoryName: data.repositoryName,
|
|
1243
|
+
branchName: data.fromBranchName,
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
if (!source) {
|
|
1247
|
+
throw new BadDataException(
|
|
1248
|
+
`Cannot branch from "${data.fromBranchName}" — no such branch in ${data.organizationName}/${data.repositoryName}.`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const tokenData: GitHubInstallationToken =
|
|
1253
|
+
await GitHubUtil.getInstallationAccessToken(data.installationId, {
|
|
1254
|
+
permissions: { contents: "write", metadata: "read" },
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post(
|
|
1258
|
+
{
|
|
1259
|
+
url: URL.fromString(
|
|
1260
|
+
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/git/refs`,
|
|
1261
|
+
),
|
|
1262
|
+
data: {
|
|
1263
|
+
ref: `refs/heads/${data.newBranchName}`,
|
|
1264
|
+
sha: source.headSha,
|
|
1265
|
+
},
|
|
1266
|
+
headers: GitHubUtil.buildApiHeaders(tokenData.token),
|
|
1267
|
+
},
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
if (result instanceof HTTPErrorResponse) {
|
|
1271
|
+
if (result.statusCode === 422) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
throw result;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/*
|
|
1281
|
+
* Open a pull request using an installation token — the App-authenticated
|
|
1282
|
+
* sibling of the instance-method createPullRequest, which authenticates from
|
|
1283
|
+
* a clone's own token.
|
|
1284
|
+
*
|
|
1285
|
+
* Opened as a DRAFT by default; GitHub answers 422 for repositories whose
|
|
1286
|
+
* plan lacks drafts, and we retry as non-draft rather than losing the work.
|
|
1287
|
+
*/
|
|
1288
|
+
@CaptureSpan()
|
|
1289
|
+
public static async createPullRequestWithToken(data: {
|
|
1290
|
+
installationId: string;
|
|
1291
|
+
organizationName: string;
|
|
1292
|
+
repositoryName: string;
|
|
1293
|
+
baseBranchName: string;
|
|
1294
|
+
headBranchName: string;
|
|
1295
|
+
title: string;
|
|
1296
|
+
body: string;
|
|
1297
|
+
isDraft?: boolean | undefined;
|
|
1298
|
+
}): Promise<PullRequest> {
|
|
1299
|
+
const tokenData: GitHubInstallationToken =
|
|
1300
|
+
await GitHubUtil.getInstallationAccessToken(data.installationId, {
|
|
1301
|
+
permissions: { pull_requests: "write", contents: "write" },
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const headers: Headers = GitHubUtil.buildApiHeaders(tokenData.token);
|
|
1305
|
+
const url: URL = URL.fromString(
|
|
1306
|
+
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/pulls`,
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
const wantsDraft: boolean = data.isDraft !== false;
|
|
1310
|
+
|
|
1311
|
+
let result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post({
|
|
1312
|
+
url: url,
|
|
1313
|
+
data: {
|
|
1314
|
+
base: data.baseBranchName,
|
|
1315
|
+
head: data.headBranchName,
|
|
1316
|
+
title: data.title,
|
|
1317
|
+
body: data.body,
|
|
1318
|
+
draft: wantsDraft,
|
|
1319
|
+
},
|
|
1320
|
+
headers: headers,
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
if (
|
|
1324
|
+
result instanceof HTTPErrorResponse &&
|
|
1325
|
+
wantsDraft &&
|
|
1326
|
+
result.statusCode === 422
|
|
1327
|
+
) {
|
|
1328
|
+
logger.debug(
|
|
1329
|
+
`GitHub rejected the draft pull request for ${data.organizationName}/${data.repositoryName}; retrying as non-draft.`,
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
result = await API.post({
|
|
1333
|
+
url: url,
|
|
1334
|
+
data: {
|
|
1335
|
+
base: data.baseBranchName,
|
|
1336
|
+
head: data.headBranchName,
|
|
1337
|
+
title: data.title,
|
|
1338
|
+
body: data.body,
|
|
1339
|
+
},
|
|
1340
|
+
headers: headers,
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (result instanceof HTTPErrorResponse) {
|
|
1345
|
+
throw result;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return new GitHubUtil({
|
|
1349
|
+
authToken: tokenData.token,
|
|
1350
|
+
username: "oneuptime",
|
|
1351
|
+
}).getPullRequestFromJSONObject({
|
|
1352
|
+
pullRequest: result.data,
|
|
1353
|
+
organizationName: data.organizationName,
|
|
1354
|
+
repositoryName: data.repositoryName,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/*
|
|
1359
|
+
* Every blob path in a tree mapped to its git file mode, so a rewrite can
|
|
1360
|
+
* preserve the mode instead of flattening everything to 100644. Recursive
|
|
1361
|
+
* because the caller writes nested paths.
|
|
1362
|
+
*
|
|
1363
|
+
* A truncated response (a tree too large for one page) is not an error here:
|
|
1364
|
+
* a path we cannot see its mode for simply falls back to the 100644 default,
|
|
1365
|
+
* which is the same behaviour as treating it as a new file.
|
|
1366
|
+
*/
|
|
1367
|
+
private static async getTreeEntryModes(data: {
|
|
1368
|
+
repoApiUrl: string;
|
|
1369
|
+
headers: Headers;
|
|
1370
|
+
treeSha: string;
|
|
1371
|
+
}): Promise<Map<string, string>> {
|
|
1372
|
+
const modes: Map<string, string> = new Map<string, string>();
|
|
1373
|
+
|
|
1374
|
+
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.get({
|
|
1375
|
+
url: URL.fromString(
|
|
1376
|
+
`${data.repoApiUrl}/git/trees/${data.treeSha}?recursive=1`,
|
|
1377
|
+
),
|
|
1378
|
+
headers: data.headers,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
if (result instanceof HTTPErrorResponse) {
|
|
1382
|
+
throw result;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const tree: JSONArray = (result.data["tree"] as JSONArray) || [];
|
|
1386
|
+
|
|
1387
|
+
for (const entry of tree) {
|
|
1388
|
+
const entryData: JSONObject = entry as JSONObject;
|
|
1389
|
+
const path: string = entryData["path"] as string;
|
|
1390
|
+
const mode: string = entryData["mode"] as string;
|
|
1391
|
+
|
|
1392
|
+
if (path && mode) {
|
|
1393
|
+
modes.set(path, mode);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
return modes;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
private static buildApiHeaders(token: string): Headers {
|
|
1401
|
+
return {
|
|
1402
|
+
Authorization: `Bearer ${token}`,
|
|
1403
|
+
Accept: "application/vnd.github+json",
|
|
1404
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
808
1408
|
/**
|
|
809
1409
|
* Verifies a GitHub webhook signature
|
|
810
1410
|
* @param payload - The raw request body
|
|
@@ -86,10 +86,15 @@ const PYTHON_FRAME_REGEX: RegExp = /File\s+"([^"]+)"\s*,\s*line\s+\d+/g;
|
|
|
86
86
|
/*
|
|
87
87
|
* Normalizes one raw path plucked from a stack frame into a relative,
|
|
88
88
|
* forward-slash path — or null when the frame is dependency/runtime noise.
|
|
89
|
+
*
|
|
90
|
+
* Exported because a runtime path from a stack frame (`/app/src/billing.ts`)
|
|
91
|
+
* is not a repository path (`src/billing.ts`): anything that hands a frame's
|
|
92
|
+
* file to a repository API must strip the container prefix first, or every
|
|
93
|
+
* lookup 404s.
|
|
89
94
|
*/
|
|
90
|
-
type NormalizeCandidatePathFunction = (rawPath: string) => string | null;
|
|
95
|
+
export type NormalizeCandidatePathFunction = (rawPath: string) => string | null;
|
|
91
96
|
|
|
92
|
-
const normalizeCandidatePath: NormalizeCandidatePathFunction = (
|
|
97
|
+
export const normalizeCandidatePath: NormalizeCandidatePathFunction = (
|
|
93
98
|
rawPath: string,
|
|
94
99
|
): string | null => {
|
|
95
100
|
let path: string = rawPath.trim().replace(/\\/g, "/");
|