@r_masseater/ops-harbor 0.1.3 → 0.1.5
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/dist/cli.js +655 -58
- package/dist/client/assets/index-88j-xN9l.js +51 -0
- package/dist/client/assets/index-B3bQ__ol.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/control-plane.js +397 -210
- package/dist/mcp-server.js +26 -27
- package/package.json +2 -1
- package/dist/client/assets/index-CaPJEHrU.js +0 -51
- package/dist/client/assets/index-avyH3PkH.css +0 -1
package/dist/control-plane.js
CHANGED
|
@@ -567,133 +567,8 @@ function deriveAlerts(item, recentEvents = []) {
|
|
|
567
567
|
function hasBlockingAlerts(item) {
|
|
568
568
|
return item.alerts.some((alert) => alert.severity !== "info");
|
|
569
569
|
}
|
|
570
|
-
// ../../packages/ops-harbor-core/src/
|
|
571
|
-
|
|
572
|
-
return workItems.map((item) => ({
|
|
573
|
-
workItemId: item.id,
|
|
574
|
-
repository: item.repository,
|
|
575
|
-
number: item.number,
|
|
576
|
-
title: item.title,
|
|
577
|
-
url: item.url,
|
|
578
|
-
alerts: item.alerts,
|
|
579
|
-
updatedAt: item.updatedAt
|
|
580
|
-
}));
|
|
581
|
-
}
|
|
582
|
-
function buildActivityWhereClause(options, columnMap = {
|
|
583
|
-
workItemId: "work_item_id",
|
|
584
|
-
repository: "repository"
|
|
585
|
-
}) {
|
|
586
|
-
const clauses = [];
|
|
587
|
-
const params = [];
|
|
588
|
-
if (options.workItemId) {
|
|
589
|
-
clauses.push(`${columnMap.workItemId} = ?`);
|
|
590
|
-
params.push(options.workItemId);
|
|
591
|
-
}
|
|
592
|
-
if (options.repository) {
|
|
593
|
-
clauses.push(`${columnMap.repository} = ?`);
|
|
594
|
-
params.push(options.repository);
|
|
595
|
-
}
|
|
596
|
-
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
597
|
-
return { where, params };
|
|
598
|
-
}
|
|
599
|
-
// ../../packages/ops-harbor-core/src/filters.ts
|
|
600
|
-
function filterWorkItems(items, filter) {
|
|
601
|
-
return items.filter((item) => {
|
|
602
|
-
if (filter.state && item.state !== filter.state)
|
|
603
|
-
return false;
|
|
604
|
-
if (filter.repository && item.repository !== filter.repository)
|
|
605
|
-
return false;
|
|
606
|
-
if (filter.hasBlocker !== undefined && hasBlockingAlerts(item) !== filter.hasBlocker) {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
if (filter.updatedSince && item.updatedAt < filter.updatedSince)
|
|
610
|
-
return false;
|
|
611
|
-
return true;
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
function parseWorkItemFilterFromQuery(getParam) {
|
|
615
|
-
const filter = {};
|
|
616
|
-
const state = getParam("state");
|
|
617
|
-
if (state === "open" || state === "closed" || state === "merged") {
|
|
618
|
-
filter.state = state;
|
|
619
|
-
}
|
|
620
|
-
const repository = getParam("repository");
|
|
621
|
-
if (repository)
|
|
622
|
-
filter.repository = repository;
|
|
623
|
-
if (getParam("has_blocker") === "true") {
|
|
624
|
-
filter.hasBlocker = true;
|
|
625
|
-
} else if (getParam("has_blocker") === "false") {
|
|
626
|
-
filter.hasBlocker = false;
|
|
627
|
-
}
|
|
628
|
-
const updatedSince = getParam("updated_since");
|
|
629
|
-
if (updatedSince)
|
|
630
|
-
filter.updatedSince = updatedSince;
|
|
631
|
-
return filter;
|
|
632
|
-
}
|
|
633
|
-
// ../../packages/ops-harbor-core/src/prompt.ts
|
|
634
|
-
var TRIGGER_INSTRUCTIONS = {
|
|
635
|
-
ci_failed: "Investigate the failing checks, implement the minimum safe fix, and rerun local validation.",
|
|
636
|
-
conflicted: "Resolve merge conflicts with the base branch while preserving branch intent.",
|
|
637
|
-
base_behind: "Update the branch with the latest base branch changes and resolve follow-up issues.",
|
|
638
|
-
review_commented: "Review the latest reviewer feedback, apply the requested follow-up if it is actionable, and summarize the changes.",
|
|
639
|
-
review_changes_requested: "Address the requested review changes, validate locally, and prepare the branch for another review."
|
|
640
|
-
};
|
|
641
|
-
function buildAutomationPrompt(workItem, trigger) {
|
|
642
|
-
return [
|
|
643
|
-
`Provider: ${workItem.provider}`,
|
|
644
|
-
`Repository: ${workItem.repository}`,
|
|
645
|
-
`Work item: #${workItem.number} ${workItem.title}`,
|
|
646
|
-
`URL: ${workItem.url}`,
|
|
647
|
-
`Base branch: ${workItem.baseBranch}`,
|
|
648
|
-
`Head branch: ${workItem.headBranch}`,
|
|
649
|
-
`Trigger: ${trigger}`,
|
|
650
|
-
"",
|
|
651
|
-
TRIGGER_INSTRUCTIONS[trigger]
|
|
652
|
-
].join(`
|
|
653
|
-
`);
|
|
654
|
-
}
|
|
655
|
-
// ../../packages/ops-harbor-core/src/port-finder.ts
|
|
656
|
-
import { createServer } from "net";
|
|
657
|
-
var MAX_PORT = 65535;
|
|
658
|
-
async function findAvailablePort(startPort) {
|
|
659
|
-
for (let port = startPort;port <= MAX_PORT; port += 1) {
|
|
660
|
-
if (await isPortAvailable(port)) {
|
|
661
|
-
return port;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
throw new Error(`No available port found between ${startPort} and ${MAX_PORT}`);
|
|
665
|
-
}
|
|
666
|
-
function isPortAvailable(port) {
|
|
667
|
-
return new Promise((resolve) => {
|
|
668
|
-
const server = createServer();
|
|
669
|
-
server.once("error", () => resolve(false));
|
|
670
|
-
server.once("listening", () => {
|
|
671
|
-
server.close(() => resolve(true));
|
|
672
|
-
});
|
|
673
|
-
server.listen(port, "127.0.0.1");
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
// ../../packages/ops-harbor-core/src/sqlite.ts
|
|
677
|
-
import { createRequire } from "module";
|
|
678
|
-
import { resolve } from "path";
|
|
679
|
-
var requireFromModule = createRequire(import.meta.url);
|
|
680
|
-
var requireFromWorkingDirectory = createRequire(resolve(process.cwd(), "package.json"));
|
|
681
|
-
function openSqliteDatabase(filename) {
|
|
682
|
-
const db = typeof Bun !== "undefined" ? new (requireFromModule("bun:sqlite")).Database(filename) : new (requireFromWorkingDirectory("better-sqlite3"))(filename);
|
|
683
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
684
|
-
return db;
|
|
685
|
-
}
|
|
686
|
-
function parseJson(value, fallback) {
|
|
687
|
-
if (!value)
|
|
688
|
-
return fallback;
|
|
689
|
-
try {
|
|
690
|
-
return JSON.parse(value);
|
|
691
|
-
} catch {
|
|
692
|
-
return fallback;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// ../ops-harbor-control-plane/src/lib/github.ts
|
|
696
|
-
import { createHmac, createSign } from "crypto";
|
|
570
|
+
// ../../packages/ops-harbor-core/src/github.ts
|
|
571
|
+
import { createSign } from "crypto";
|
|
697
572
|
var SEARCH_QUERY = `
|
|
698
573
|
query SearchPullRequests($query: String!, $first: Int!, $after: String) {
|
|
699
574
|
search(query: $query, type: ISSUE, first: $first, after: $after) {
|
|
@@ -836,6 +711,202 @@ function createAppJwt(appId, privateKeyPem) {
|
|
|
836
711
|
const signature = signer.sign(privateKeyPem);
|
|
837
712
|
return `${encoded}.${toBase64Url(signature)}`;
|
|
838
713
|
}
|
|
714
|
+
function mapChecks(node) {
|
|
715
|
+
const contexts = node.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
|
|
716
|
+
return contexts.map((context) => {
|
|
717
|
+
if (context.__typename === "CheckRun") {
|
|
718
|
+
return {
|
|
719
|
+
name: context.name,
|
|
720
|
+
status: context.status,
|
|
721
|
+
conclusion: context.conclusion,
|
|
722
|
+
url: context.detailsUrl ?? null,
|
|
723
|
+
startedAt: context.startedAt ?? null,
|
|
724
|
+
completedAt: context.completedAt ?? null
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
name: context.context,
|
|
729
|
+
status: context.state === "PENDING" ? "pending" : "completed",
|
|
730
|
+
conclusion: context.state.toLowerCase(),
|
|
731
|
+
url: context.targetUrl ?? null,
|
|
732
|
+
startedAt: context.createdAt ?? null,
|
|
733
|
+
completedAt: context.createdAt ?? null
|
|
734
|
+
};
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function mapReviews(node) {
|
|
738
|
+
return node.latestReviews.nodes.map((review) => ({
|
|
739
|
+
id: review.id,
|
|
740
|
+
state: review.state.toLowerCase(),
|
|
741
|
+
author: review.author?.login ?? "unknown",
|
|
742
|
+
submittedAt: review.submittedAt,
|
|
743
|
+
...review.body ? { bodySnippet: review.body.slice(0, 240) } : {},
|
|
744
|
+
...review.url ? { url: review.url } : {}
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
function mapPullRequestNode(node, installationId) {
|
|
748
|
+
const checks = mapChecks(node);
|
|
749
|
+
const reviews = mapReviews(node);
|
|
750
|
+
const statusSummary = summarizeStatus(checks, reviews, {
|
|
751
|
+
mergeable: node.mergeable === "CONFLICTING" ? "conflicting" : node.mergeable === "MERGEABLE" ? "mergeable" : "unknown",
|
|
752
|
+
mergeStateStatus: node.mergeStateStatus,
|
|
753
|
+
reviewDecision: node.reviewDecision
|
|
754
|
+
});
|
|
755
|
+
return {
|
|
756
|
+
id: node.id,
|
|
757
|
+
provider: "github",
|
|
758
|
+
kind: "pull_request",
|
|
759
|
+
repository: node.repository.nameWithOwner,
|
|
760
|
+
number: node.number,
|
|
761
|
+
title: node.title,
|
|
762
|
+
url: node.url,
|
|
763
|
+
state: node.state.toLowerCase(),
|
|
764
|
+
author: node.author?.login ?? "unknown",
|
|
765
|
+
isDraft: node.isDraft,
|
|
766
|
+
headBranch: node.headRefName,
|
|
767
|
+
headSha: node.headRefOid,
|
|
768
|
+
baseBranch: node.baseRefName,
|
|
769
|
+
mergeable: node.mergeable === "CONFLICTING" ? "conflicting" : node.mergeable === "MERGEABLE" ? "mergeable" : "unknown",
|
|
770
|
+
mergeStateStatus: node.mergeStateStatus,
|
|
771
|
+
reviewDecision: node.reviewDecision,
|
|
772
|
+
statusSummary,
|
|
773
|
+
checks,
|
|
774
|
+
reviews,
|
|
775
|
+
alerts: [],
|
|
776
|
+
lastActivityAt: node.updatedAt,
|
|
777
|
+
updatedAt: node.updatedAt,
|
|
778
|
+
providerPayload: {
|
|
779
|
+
installationId,
|
|
780
|
+
repositoryUrl: node.repository.url
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
// ../../packages/ops-harbor-core/src/db-helpers.ts
|
|
785
|
+
function mapWorkItemsToAlertSummaries(workItems) {
|
|
786
|
+
return workItems.map((item) => ({
|
|
787
|
+
workItemId: item.id,
|
|
788
|
+
repository: item.repository,
|
|
789
|
+
number: item.number,
|
|
790
|
+
title: item.title,
|
|
791
|
+
url: item.url,
|
|
792
|
+
alerts: item.alerts,
|
|
793
|
+
updatedAt: item.updatedAt
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
function buildActivityWhereClause(options, columnMap = {
|
|
797
|
+
workItemId: "work_item_id",
|
|
798
|
+
repository: "repository"
|
|
799
|
+
}) {
|
|
800
|
+
const clauses = [];
|
|
801
|
+
const params = [];
|
|
802
|
+
if (options.workItemId) {
|
|
803
|
+
clauses.push(`${columnMap.workItemId} = ?`);
|
|
804
|
+
params.push(options.workItemId);
|
|
805
|
+
}
|
|
806
|
+
if (options.repository) {
|
|
807
|
+
clauses.push(`${columnMap.repository} = ?`);
|
|
808
|
+
params.push(options.repository);
|
|
809
|
+
}
|
|
810
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
811
|
+
return { where, params };
|
|
812
|
+
}
|
|
813
|
+
// ../../packages/ops-harbor-core/src/filters.ts
|
|
814
|
+
function filterWorkItems(items, filter) {
|
|
815
|
+
return items.filter((item) => {
|
|
816
|
+
if (filter.state && item.state !== filter.state)
|
|
817
|
+
return false;
|
|
818
|
+
if (filter.repository && item.repository !== filter.repository)
|
|
819
|
+
return false;
|
|
820
|
+
if (filter.hasBlocker !== undefined && hasBlockingAlerts(item) !== filter.hasBlocker) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
if (filter.updatedSince && item.updatedAt < filter.updatedSince)
|
|
824
|
+
return false;
|
|
825
|
+
return true;
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function parseWorkItemFilterFromQuery(getParam) {
|
|
829
|
+
const filter = {};
|
|
830
|
+
const state = getParam("state");
|
|
831
|
+
if (state === "open" || state === "closed" || state === "merged") {
|
|
832
|
+
filter.state = state;
|
|
833
|
+
}
|
|
834
|
+
const repository = getParam("repository");
|
|
835
|
+
if (repository)
|
|
836
|
+
filter.repository = repository;
|
|
837
|
+
if (getParam("has_blocker") === "true") {
|
|
838
|
+
filter.hasBlocker = true;
|
|
839
|
+
} else if (getParam("has_blocker") === "false") {
|
|
840
|
+
filter.hasBlocker = false;
|
|
841
|
+
}
|
|
842
|
+
const updatedSince = getParam("updated_since");
|
|
843
|
+
if (updatedSince)
|
|
844
|
+
filter.updatedSince = updatedSince;
|
|
845
|
+
return filter;
|
|
846
|
+
}
|
|
847
|
+
// ../../packages/ops-harbor-core/src/prompt.ts
|
|
848
|
+
var TRIGGER_INSTRUCTIONS = {
|
|
849
|
+
ci_failed: "Investigate the failing checks, implement the minimum safe fix, and rerun local validation.",
|
|
850
|
+
conflicted: "Resolve merge conflicts with the base branch while preserving branch intent.",
|
|
851
|
+
base_behind: "Update the branch with the latest base branch changes and resolve follow-up issues.",
|
|
852
|
+
review_commented: "Review the latest reviewer feedback, apply the requested follow-up if it is actionable, and summarize the changes.",
|
|
853
|
+
review_changes_requested: "Address the requested review changes, validate locally, and prepare the branch for another review."
|
|
854
|
+
};
|
|
855
|
+
function buildAutomationPrompt(workItem, trigger) {
|
|
856
|
+
return [
|
|
857
|
+
`Provider: ${workItem.provider}`,
|
|
858
|
+
`Repository: ${workItem.repository}`,
|
|
859
|
+
`Work item: #${workItem.number} ${workItem.title}`,
|
|
860
|
+
`URL: ${workItem.url}`,
|
|
861
|
+
`Base branch: ${workItem.baseBranch}`,
|
|
862
|
+
`Head branch: ${workItem.headBranch}`,
|
|
863
|
+
`Trigger: ${trigger}`,
|
|
864
|
+
"",
|
|
865
|
+
TRIGGER_INSTRUCTIONS[trigger]
|
|
866
|
+
].join(`
|
|
867
|
+
`);
|
|
868
|
+
}
|
|
869
|
+
// ../../packages/ops-harbor-core/src/port-finder.ts
|
|
870
|
+
import { createServer } from "net";
|
|
871
|
+
var MAX_PORT = 65535;
|
|
872
|
+
async function findAvailablePort(startPort) {
|
|
873
|
+
for (let port = startPort;port <= MAX_PORT; port += 1) {
|
|
874
|
+
if (await isPortAvailable(port)) {
|
|
875
|
+
return port;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
throw new Error(`No available port found between ${startPort} and ${MAX_PORT}`);
|
|
879
|
+
}
|
|
880
|
+
function isPortAvailable(port) {
|
|
881
|
+
return new Promise((resolve) => {
|
|
882
|
+
const server = createServer();
|
|
883
|
+
server.once("error", () => resolve(false));
|
|
884
|
+
server.once("listening", () => {
|
|
885
|
+
server.close(() => resolve(true));
|
|
886
|
+
});
|
|
887
|
+
server.listen(port, "127.0.0.1");
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
// ../../packages/ops-harbor-core/src/sqlite.ts
|
|
891
|
+
import { createRequire } from "module";
|
|
892
|
+
var overriddenCtor;
|
|
893
|
+
function openSqliteDatabase(filename) {
|
|
894
|
+
const Ctor = overriddenCtor ?? createRequire(import.meta.url)("bun:sqlite").Database;
|
|
895
|
+
const db = new Ctor(filename);
|
|
896
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
897
|
+
return db;
|
|
898
|
+
}
|
|
899
|
+
function parseJson(value, fallback) {
|
|
900
|
+
if (!value)
|
|
901
|
+
return fallback;
|
|
902
|
+
try {
|
|
903
|
+
return JSON.parse(value);
|
|
904
|
+
} catch {
|
|
905
|
+
return fallback;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// ../ops-harbor-control-plane/src/lib/github.ts
|
|
909
|
+
import { createHmac } from "crypto";
|
|
839
910
|
async function githubRequest(config, path, init, scope) {
|
|
840
911
|
const response = await fetch(`${config.githubApiUrl}${path}`, {
|
|
841
912
|
...init,
|
|
@@ -957,7 +1028,7 @@ async function ensureGitHubAppWebhookUrl(config, webhookUrl) {
|
|
|
957
1028
|
rateLimit: mergeRateLimitSnapshots(current.rateLimit, next.rateLimit)
|
|
958
1029
|
};
|
|
959
1030
|
}
|
|
960
|
-
async function
|
|
1031
|
+
async function createInstallationToken2(config, installationId) {
|
|
961
1032
|
const { data, rateLimit } = await githubRequest(config, `/app/installations/${installationId}/access_tokens`, {
|
|
962
1033
|
method: "POST",
|
|
963
1034
|
headers: {
|
|
@@ -967,7 +1038,7 @@ async function createInstallationToken(config, installationId) {
|
|
|
967
1038
|
return { token: data.token, rateLimit };
|
|
968
1039
|
}
|
|
969
1040
|
async function graphql(config, installationId, query, variables) {
|
|
970
|
-
const { token } = await
|
|
1041
|
+
const { token } = await createInstallationToken2(config, installationId);
|
|
971
1042
|
const response = await fetch(`${config.githubApiUrl}/graphql`, {
|
|
972
1043
|
method: "POST",
|
|
973
1044
|
headers: {
|
|
@@ -996,77 +1067,7 @@ async function graphql(config, installationId, query, variables) {
|
|
|
996
1067
|
}
|
|
997
1068
|
};
|
|
998
1069
|
}
|
|
999
|
-
function
|
|
1000
|
-
const contexts = node.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
|
|
1001
|
-
return contexts.map((context) => {
|
|
1002
|
-
if (context.__typename === "CheckRun") {
|
|
1003
|
-
return {
|
|
1004
|
-
name: context.name,
|
|
1005
|
-
status: context.status,
|
|
1006
|
-
conclusion: context.conclusion,
|
|
1007
|
-
url: context.detailsUrl ?? null,
|
|
1008
|
-
startedAt: context.startedAt ?? null,
|
|
1009
|
-
completedAt: context.completedAt ?? null
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
return {
|
|
1013
|
-
name: context.context,
|
|
1014
|
-
status: context.state === "PENDING" ? "pending" : "completed",
|
|
1015
|
-
conclusion: context.state.toLowerCase(),
|
|
1016
|
-
url: context.targetUrl ?? null,
|
|
1017
|
-
startedAt: context.createdAt ?? null,
|
|
1018
|
-
completedAt: context.createdAt ?? null
|
|
1019
|
-
};
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
function mapReviews(node) {
|
|
1023
|
-
return node.latestReviews.nodes.map((review) => ({
|
|
1024
|
-
id: review.id,
|
|
1025
|
-
state: review.state.toLowerCase(),
|
|
1026
|
-
author: review.author?.login ?? "unknown",
|
|
1027
|
-
submittedAt: review.submittedAt,
|
|
1028
|
-
...review.body ? { bodySnippet: review.body.slice(0, 240) } : {},
|
|
1029
|
-
...review.url ? { url: review.url } : {}
|
|
1030
|
-
}));
|
|
1031
|
-
}
|
|
1032
|
-
function mapPullRequest(node, installationId) {
|
|
1033
|
-
const checks = mapChecks(node);
|
|
1034
|
-
const reviews = mapReviews(node);
|
|
1035
|
-
const statusSummary = summarizeStatus(checks, reviews, {
|
|
1036
|
-
mergeable: node.mergeable === "CONFLICTING" ? "conflicting" : node.mergeable === "MERGEABLE" ? "mergeable" : "unknown",
|
|
1037
|
-
mergeStateStatus: node.mergeStateStatus,
|
|
1038
|
-
reviewDecision: node.reviewDecision
|
|
1039
|
-
});
|
|
1040
|
-
return {
|
|
1041
|
-
id: node.id,
|
|
1042
|
-
provider: "github",
|
|
1043
|
-
kind: "pull_request",
|
|
1044
|
-
repository: node.repository.nameWithOwner,
|
|
1045
|
-
number: node.number,
|
|
1046
|
-
title: node.title,
|
|
1047
|
-
url: node.url,
|
|
1048
|
-
state: node.state.toLowerCase(),
|
|
1049
|
-
author: node.author?.login ?? "unknown",
|
|
1050
|
-
isDraft: node.isDraft,
|
|
1051
|
-
headBranch: node.headRefName,
|
|
1052
|
-
headSha: node.headRefOid,
|
|
1053
|
-
baseBranch: node.baseRefName,
|
|
1054
|
-
mergeable: node.mergeable === "CONFLICTING" ? "conflicting" : node.mergeable === "MERGEABLE" ? "mergeable" : "unknown",
|
|
1055
|
-
mergeStateStatus: node.mergeStateStatus,
|
|
1056
|
-
reviewDecision: node.reviewDecision,
|
|
1057
|
-
statusSummary,
|
|
1058
|
-
checks,
|
|
1059
|
-
reviews,
|
|
1060
|
-
alerts: [],
|
|
1061
|
-
lastActivityAt: node.updatedAt,
|
|
1062
|
-
updatedAt: node.updatedAt,
|
|
1063
|
-
providerPayload: {
|
|
1064
|
-
installationId,
|
|
1065
|
-
repositoryUrl: node.repository.url
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
async function fetchOpenPullRequestsForAuthor(config, installationId, author, limit = 100) {
|
|
1070
|
+
async function fetchOpenPullRequestsForAuthor2(config, installationId, author, limit = 100) {
|
|
1070
1071
|
const items = [];
|
|
1071
1072
|
let cursor = null;
|
|
1072
1073
|
let lastRateLimit = {};
|
|
@@ -1078,7 +1079,7 @@ async function fetchOpenPullRequestsForAuthor(config, installationId, author, li
|
|
|
1078
1079
|
});
|
|
1079
1080
|
const data = response.data;
|
|
1080
1081
|
lastRateLimit = response.rateLimit;
|
|
1081
|
-
items.push(...data.search.nodes.map((node) =>
|
|
1082
|
+
items.push(...data.search.nodes.map((node) => mapPullRequestNode(node, installationId)));
|
|
1082
1083
|
if (!data.search.pageInfo.hasNextPage)
|
|
1083
1084
|
break;
|
|
1084
1085
|
cursor = data.search.pageInfo.endCursor;
|
|
@@ -1092,7 +1093,7 @@ async function hydratePullRequest(config, installationId, repository, number2) {
|
|
|
1092
1093
|
}
|
|
1093
1094
|
const { data, rateLimit } = await graphql(config, installationId, PULL_REQUEST_QUERY, { owner, repo, number: number2 });
|
|
1094
1095
|
return {
|
|
1095
|
-
item: data.repository.pullRequest ?
|
|
1096
|
+
item: data.repository.pullRequest ? mapPullRequestNode(data.repository.pullRequest, installationId) : null,
|
|
1096
1097
|
rateLimit
|
|
1097
1098
|
};
|
|
1098
1099
|
}
|
|
@@ -2749,6 +2750,148 @@ var cors = (options) => {
|
|
|
2749
2750
|
};
|
|
2750
2751
|
};
|
|
2751
2752
|
|
|
2753
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/utils/stream.js
|
|
2754
|
+
var StreamingApi = class {
|
|
2755
|
+
writer;
|
|
2756
|
+
encoder;
|
|
2757
|
+
writable;
|
|
2758
|
+
abortSubscribers = [];
|
|
2759
|
+
responseReadable;
|
|
2760
|
+
aborted = false;
|
|
2761
|
+
closed = false;
|
|
2762
|
+
constructor(writable, _readable) {
|
|
2763
|
+
this.writable = writable;
|
|
2764
|
+
this.writer = writable.getWriter();
|
|
2765
|
+
this.encoder = new TextEncoder;
|
|
2766
|
+
const reader = _readable.getReader();
|
|
2767
|
+
this.abortSubscribers.push(async () => {
|
|
2768
|
+
await reader.cancel();
|
|
2769
|
+
});
|
|
2770
|
+
this.responseReadable = new ReadableStream({
|
|
2771
|
+
async pull(controller) {
|
|
2772
|
+
const { done, value } = await reader.read();
|
|
2773
|
+
done ? controller.close() : controller.enqueue(value);
|
|
2774
|
+
},
|
|
2775
|
+
cancel: () => {
|
|
2776
|
+
this.abort();
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
async write(input) {
|
|
2781
|
+
try {
|
|
2782
|
+
if (typeof input === "string") {
|
|
2783
|
+
input = this.encoder.encode(input);
|
|
2784
|
+
}
|
|
2785
|
+
await this.writer.write(input);
|
|
2786
|
+
} catch {}
|
|
2787
|
+
return this;
|
|
2788
|
+
}
|
|
2789
|
+
async writeln(input) {
|
|
2790
|
+
await this.write(input + `
|
|
2791
|
+
`);
|
|
2792
|
+
return this;
|
|
2793
|
+
}
|
|
2794
|
+
sleep(ms) {
|
|
2795
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
2796
|
+
}
|
|
2797
|
+
async close() {
|
|
2798
|
+
try {
|
|
2799
|
+
await this.writer.close();
|
|
2800
|
+
} catch {}
|
|
2801
|
+
this.closed = true;
|
|
2802
|
+
}
|
|
2803
|
+
async pipe(body) {
|
|
2804
|
+
this.writer.releaseLock();
|
|
2805
|
+
await body.pipeTo(this.writable, { preventClose: true });
|
|
2806
|
+
this.writer = this.writable.getWriter();
|
|
2807
|
+
}
|
|
2808
|
+
onAbort(listener) {
|
|
2809
|
+
this.abortSubscribers.push(listener);
|
|
2810
|
+
}
|
|
2811
|
+
abort() {
|
|
2812
|
+
if (!this.aborted) {
|
|
2813
|
+
this.aborted = true;
|
|
2814
|
+
this.abortSubscribers.forEach((subscriber) => subscriber());
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2819
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/helper/streaming/utils.js
|
|
2820
|
+
var isOldBunVersion = () => {
|
|
2821
|
+
const version = typeof Bun !== "undefined" ? Bun.version : undefined;
|
|
2822
|
+
if (version === undefined) {
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
|
|
2826
|
+
isOldBunVersion = () => result;
|
|
2827
|
+
return result;
|
|
2828
|
+
};
|
|
2829
|
+
|
|
2830
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/helper/streaming/sse.js
|
|
2831
|
+
var SSEStreamingApi = class extends StreamingApi {
|
|
2832
|
+
constructor(writable, readable) {
|
|
2833
|
+
super(writable, readable);
|
|
2834
|
+
}
|
|
2835
|
+
async writeSSE(message) {
|
|
2836
|
+
const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
|
|
2837
|
+
const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
|
|
2838
|
+
return `data: ${line}`;
|
|
2839
|
+
}).join(`
|
|
2840
|
+
`);
|
|
2841
|
+
for (const key of ["event", "id", "retry"]) {
|
|
2842
|
+
if (message[key] && /[\r\n]/.test(message[key])) {
|
|
2843
|
+
throw new Error(`${key} must not contain "\\r" or "\\n"`);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
const sseData = [
|
|
2847
|
+
message.event && `event: ${message.event}`,
|
|
2848
|
+
dataLines,
|
|
2849
|
+
message.id && `id: ${message.id}`,
|
|
2850
|
+
message.retry && `retry: ${message.retry}`
|
|
2851
|
+
].filter(Boolean).join(`
|
|
2852
|
+
`) + `
|
|
2853
|
+
|
|
2854
|
+
`;
|
|
2855
|
+
await this.write(sseData);
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
var run = async (stream, cb, onError) => {
|
|
2859
|
+
try {
|
|
2860
|
+
await cb(stream);
|
|
2861
|
+
} catch (e) {
|
|
2862
|
+
if (e instanceof Error && onError) {
|
|
2863
|
+
await onError(e, stream);
|
|
2864
|
+
await stream.writeSSE({
|
|
2865
|
+
event: "error",
|
|
2866
|
+
data: e.message
|
|
2867
|
+
});
|
|
2868
|
+
} else {
|
|
2869
|
+
console.error(e);
|
|
2870
|
+
}
|
|
2871
|
+
} finally {
|
|
2872
|
+
stream.close();
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
var contextStash = /* @__PURE__ */ new WeakMap;
|
|
2876
|
+
var streamSSE = (c, cb, onError) => {
|
|
2877
|
+
const { readable, writable } = new TransformStream;
|
|
2878
|
+
const stream = new SSEStreamingApi(writable, readable);
|
|
2879
|
+
if (isOldBunVersion()) {
|
|
2880
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
2881
|
+
if (!stream.closed) {
|
|
2882
|
+
stream.abort();
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
contextStash.set(stream.responseReadable, c);
|
|
2887
|
+
c.header("Transfer-Encoding", "chunked");
|
|
2888
|
+
c.header("Content-Type", "text/event-stream");
|
|
2889
|
+
c.header("Cache-Control", "no-cache");
|
|
2890
|
+
c.header("Connection", "keep-alive");
|
|
2891
|
+
run(stream, cb, onError);
|
|
2892
|
+
return c.newResponse(stream.responseReadable);
|
|
2893
|
+
};
|
|
2894
|
+
|
|
2752
2895
|
// ../ops-harbor-control-plane/src/lib/db.ts
|
|
2753
2896
|
import { mkdirSync } from "fs";
|
|
2754
2897
|
import { dirname as dirname2 } from "path";
|
|
@@ -3277,7 +3420,7 @@ async function syncAuthorWorkItems(db, config, author) {
|
|
|
3277
3420
|
let changed = 0;
|
|
3278
3421
|
for (const installation of installations) {
|
|
3279
3422
|
upsertInstallation(db, installation.id, installation.account?.login, installation.account?.type);
|
|
3280
|
-
const { items, rateLimit: searchRateLimit } = await
|
|
3423
|
+
const { items, rateLimit: searchRateLimit } = await fetchOpenPullRequestsForAuthor2(config, installation.id, author);
|
|
3281
3424
|
recordRateLimit(db, `installation.${installation.id}.search`, searchRateLimit);
|
|
3282
3425
|
for (const item of items) {
|
|
3283
3426
|
const existing = getWorkItem(db, item.id);
|
|
@@ -3309,6 +3452,8 @@ async function hydrateAndPersist(db, config, installationId, repository, number2
|
|
|
3309
3452
|
recordRateLimit(db, `installation.${installationId}.hydrate`, rateLimit);
|
|
3310
3453
|
if (!item)
|
|
3311
3454
|
return;
|
|
3455
|
+
if (config.defaultAuthor && item.author !== config.defaultAuthor)
|
|
3456
|
+
return;
|
|
3312
3457
|
await saveHydratedWorkItem(db, item, event);
|
|
3313
3458
|
}
|
|
3314
3459
|
async function handleWebhookEvent(db, config, eventName, payload) {
|
|
@@ -3370,6 +3515,19 @@ async function handleWebhookEvent(db, config, eventName, payload) {
|
|
|
3370
3515
|
}
|
|
3371
3516
|
|
|
3372
3517
|
// ../ops-harbor-control-plane/src/server.ts
|
|
3518
|
+
function createEventBus() {
|
|
3519
|
+
const listeners = new Set;
|
|
3520
|
+
return {
|
|
3521
|
+
subscribe(listener) {
|
|
3522
|
+
listeners.add(listener);
|
|
3523
|
+
return () => listeners.delete(listener);
|
|
3524
|
+
},
|
|
3525
|
+
broadcast(event, data) {
|
|
3526
|
+
for (const listener of listeners)
|
|
3527
|
+
listener(event, data);
|
|
3528
|
+
}
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3373
3531
|
function fallbackRuntimeStatus() {
|
|
3374
3532
|
return {
|
|
3375
3533
|
tunnelEnabled: false,
|
|
@@ -3382,9 +3540,9 @@ function redactStoredConfig(config) {
|
|
|
3382
3540
|
const { githubWebhookSecret: _githubWebhookSecret, ...rest } = config;
|
|
3383
3541
|
return rest;
|
|
3384
3542
|
}
|
|
3385
|
-
function createControlPlaneApp({ config, db, getRuntimeStatus }) {
|
|
3543
|
+
function createControlPlaneApp({ config, db, bus, getRuntimeStatus }) {
|
|
3386
3544
|
const app = new Hono2;
|
|
3387
|
-
const webhookHandler = createGitHubWebhookHandler({ config, db });
|
|
3545
|
+
const webhookHandler = createGitHubWebhookHandler({ config, db, bus });
|
|
3388
3546
|
app.use("/api/*", cors());
|
|
3389
3547
|
app.use("/api/admin/*", async (c, next) => {
|
|
3390
3548
|
if (config.internalApiToken && c.req.header("x-ops-harbor-token") !== config.internalApiToken) {
|
|
@@ -3398,6 +3556,28 @@ function createControlPlaneApp({ config, db, getRuntimeStatus }) {
|
|
|
3398
3556
|
}
|
|
3399
3557
|
await next();
|
|
3400
3558
|
});
|
|
3559
|
+
app.get("/api/events", (c) => {
|
|
3560
|
+
const token = c.req.header("x-ops-harbor-token") ?? c.req.query("token");
|
|
3561
|
+
if (config.internalApiToken && token !== config.internalApiToken) {
|
|
3562
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
3563
|
+
}
|
|
3564
|
+
return streamSSE(c, async (stream2) => {
|
|
3565
|
+
await stream2.writeSSE({ data: "", retry: 3000 });
|
|
3566
|
+
const unsubscribe = bus.subscribe((event, data) => {
|
|
3567
|
+
stream2.writeSSE({ event, data: JSON.stringify(data) });
|
|
3568
|
+
});
|
|
3569
|
+
const heartbeat = setInterval(() => {
|
|
3570
|
+
stream2.writeSSE({ event: "heartbeat", data: "{}" });
|
|
3571
|
+
}, 15000);
|
|
3572
|
+
stream2.onAbort(() => {
|
|
3573
|
+
clearInterval(heartbeat);
|
|
3574
|
+
unsubscribe();
|
|
3575
|
+
});
|
|
3576
|
+
await new Promise(() => {});
|
|
3577
|
+
}, async (error) => {
|
|
3578
|
+
console.error("SSE stream error:", error);
|
|
3579
|
+
});
|
|
3580
|
+
});
|
|
3401
3581
|
app.get("/api/health", (c) => c.json({
|
|
3402
3582
|
ok: true,
|
|
3403
3583
|
provider: "github",
|
|
@@ -3441,6 +3621,7 @@ function createControlPlaneApp({ config, db, getRuntimeStatus }) {
|
|
|
3441
3621
|
return c.json({ error: "author is required" }, 400);
|
|
3442
3622
|
try {
|
|
3443
3623
|
const result = await syncAuthorWorkItems(db, config, author);
|
|
3624
|
+
bus.broadcast("sync_completed", { synchronized: result.synchronized });
|
|
3444
3625
|
return c.json(result);
|
|
3445
3626
|
} catch (error) {
|
|
3446
3627
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -3461,7 +3642,7 @@ function createControlPlaneApp({ config, db, getRuntimeStatus }) {
|
|
|
3461
3642
|
});
|
|
3462
3643
|
return app;
|
|
3463
3644
|
}
|
|
3464
|
-
function createGitHubWebhookHandler({ config, db }) {
|
|
3645
|
+
function createGitHubWebhookHandler({ config, db, bus }) {
|
|
3465
3646
|
return async (c) => {
|
|
3466
3647
|
const rawBody = await c.req.text();
|
|
3467
3648
|
if (config.githubWebhookSecret) {
|
|
@@ -3484,15 +3665,18 @@ function createGitHubWebhookHandler({ config, db }) {
|
|
|
3484
3665
|
try {
|
|
3485
3666
|
await handleWebhookEvent(db, config, eventName, payload);
|
|
3486
3667
|
markWebhookDeliveryProcessed(db, deliveryId);
|
|
3668
|
+
const repoObj = payload.repository;
|
|
3669
|
+
const repository = typeof repoObj === "object" && repoObj !== null ? String(repoObj.full_name ?? "") : "";
|
|
3670
|
+
bus.broadcast("webhook_processed", { repository, event: eventName });
|
|
3487
3671
|
return c.json({ ok: true });
|
|
3488
3672
|
} catch (error) {
|
|
3489
3673
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
3490
3674
|
}
|
|
3491
3675
|
};
|
|
3492
3676
|
}
|
|
3493
|
-
function createWebhookIngressApp({ config, db, getRuntimeStatus }) {
|
|
3677
|
+
function createWebhookIngressApp({ config, db, bus, getRuntimeStatus }) {
|
|
3494
3678
|
const app = new Hono2;
|
|
3495
|
-
const webhookHandler = createGitHubWebhookHandler({ config, db });
|
|
3679
|
+
const webhookHandler = createGitHubWebhookHandler({ config, db, bus });
|
|
3496
3680
|
app.get("/api/health", (c) => c.json({
|
|
3497
3681
|
ok: true,
|
|
3498
3682
|
ingress: "github-webhooks",
|
|
@@ -3503,10 +3687,11 @@ function createWebhookIngressApp({ config, db, getRuntimeStatus }) {
|
|
|
3503
3687
|
}
|
|
3504
3688
|
function openControlPlaneApps(config, getRuntimeStatus) {
|
|
3505
3689
|
const db = openControlPlaneDb(config.dbPath);
|
|
3690
|
+
const bus = createEventBus();
|
|
3506
3691
|
return {
|
|
3507
3692
|
db,
|
|
3508
|
-
controlPlaneApp: createControlPlaneApp({ config, db, getRuntimeStatus }),
|
|
3509
|
-
webhookIngressApp: createWebhookIngressApp({ config, db, getRuntimeStatus })
|
|
3693
|
+
controlPlaneApp: createControlPlaneApp({ config, db, bus, getRuntimeStatus }),
|
|
3694
|
+
webhookIngressApp: createWebhookIngressApp({ config, db, bus, getRuntimeStatus })
|
|
3510
3695
|
};
|
|
3511
3696
|
}
|
|
3512
3697
|
|
|
@@ -3527,6 +3712,7 @@ async function main() {
|
|
|
3527
3712
|
Bun.serve({
|
|
3528
3713
|
port: config.port,
|
|
3529
3714
|
hostname: "127.0.0.1",
|
|
3715
|
+
idleTimeout: 0,
|
|
3530
3716
|
fetch: controlPlaneApp.fetch
|
|
3531
3717
|
});
|
|
3532
3718
|
let tunnelSession = null;
|
|
@@ -3536,6 +3722,7 @@ async function main() {
|
|
|
3536
3722
|
Bun.serve({
|
|
3537
3723
|
port: webhookIngressPort,
|
|
3538
3724
|
hostname: "127.0.0.1",
|
|
3725
|
+
idleTimeout: 0,
|
|
3539
3726
|
fetch: webhookIngressApp.fetch
|
|
3540
3727
|
});
|
|
3541
3728
|
tunnelSession = await openTunnel({
|