paperclip-github-plugin 0.5.3 → 0.6.1
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 +51 -2
- package/dist/manifest.js +21 -2
- package/dist/ui/index.js +589 -51
- package/dist/ui/index.js.map +3 -3
- package/dist/worker.js +1187 -46
- package/package.json +3 -3
package/dist/worker.js
CHANGED
|
@@ -561,6 +561,12 @@ function parseRepositoryReference(repositoryInput) {
|
|
|
561
561
|
}
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
// src/kpi-contract.ts
|
|
565
|
+
var GITHUB_SYNC_PLUGIN_ID = "paperclip-github-plugin";
|
|
566
|
+
var COMPANY_METRIC_WEBHOOK_ENDPOINT_KEY = "record-company-metric-event";
|
|
567
|
+
var COMPANY_METRIC_WEBHOOK_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/webhooks/${COMPANY_METRIC_WEBHOOK_ENDPOINT_KEY}`;
|
|
568
|
+
var COMPANY_METRIC_WEBHOOK_AUTH_HEADER = "authorization";
|
|
569
|
+
|
|
564
570
|
// src/paperclip-health.ts
|
|
565
571
|
function normalizeOptionalString(value) {
|
|
566
572
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
@@ -604,6 +610,10 @@ var IMPORT_REGISTRY_SCOPE = {
|
|
|
604
610
|
scopeKind: "instance",
|
|
605
611
|
stateKey: "paperclip-github-plugin-import-registry"
|
|
606
612
|
};
|
|
613
|
+
var COMPANY_KPI_SCOPE = {
|
|
614
|
+
scopeKind: "instance",
|
|
615
|
+
stateKey: "paperclip-github-plugin-company-kpis"
|
|
616
|
+
};
|
|
607
617
|
var DEFAULT_SCHEDULE_FREQUENCY_MINUTES = 15;
|
|
608
618
|
var DEFAULT_IMPORTED_ISSUE_STATUS = "backlog";
|
|
609
619
|
var DEFAULT_IGNORED_GITHUB_ISSUE_USERNAMES = ["renovate"];
|
|
@@ -629,6 +639,11 @@ var SYNC_PROGRESS_PERSIST_INTERVAL_MS = 250;
|
|
|
629
639
|
var MAX_SYNC_FAILURE_LOG_ENTRIES = 25;
|
|
630
640
|
var GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_MS = 6e4;
|
|
631
641
|
var IMPORTED_ISSUE_WAKEUP_CONCURRENCY = 4;
|
|
642
|
+
var COMPANY_KPI_CHART_WINDOW_DAYS = 14;
|
|
643
|
+
var COMPANY_KPI_COMPARISON_WINDOW_DAYS = 30;
|
|
644
|
+
var MAX_COMPANY_BACKLOG_SNAPSHOTS = 180;
|
|
645
|
+
var MAX_COMPANY_ACTIVITY_ROLLUPS = 365;
|
|
646
|
+
var MAX_COMPANY_METRIC_EVENT_KEYS = 2e3;
|
|
632
647
|
var MISSING_GITHUB_TOKEN_SYNC_MESSAGE = "Configure a GitHub token before running sync.";
|
|
633
648
|
var MISSING_GITHUB_TOKEN_SYNC_ACTION = 'Open settings and save a GitHub token secret, or create $PAPERCLIP_HOME/plugins/github-sync/config.json (or ~/.paperclip/plugins/github-sync/config.json when PAPERCLIP_HOME is unset) with a "githubToken" value, and then run sync again.';
|
|
634
649
|
var MISSING_MAPPING_SYNC_MESSAGE = "Save at least one mapping with a created Paperclip project before running sync.";
|
|
@@ -646,6 +661,7 @@ var AI_AUTHORED_MARKDOWN_FOOTER_PATTERN = /\n\n---\n###### ✨ This (?:comment|i
|
|
|
646
661
|
var HIDDEN_GITHUB_IMPORT_MARKER_PREFIX = "<!-- paperclip-github-plugin-imported-from: ";
|
|
647
662
|
var HIDDEN_GITHUB_IMPORT_MARKER_SUFFIX = " -->";
|
|
648
663
|
var EMPTY_GITHUB_ISSUE_DESCRIPTION_PLACEHOLDER = "_No description provided on GitHub._";
|
|
664
|
+
var pluginRuntimeContext = null;
|
|
649
665
|
function normalizeCompanyId(value) {
|
|
650
666
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
651
667
|
}
|
|
@@ -2528,6 +2544,144 @@ async function buildToolbarSyncState(ctx, input) {
|
|
|
2528
2544
|
savedMappingCount
|
|
2529
2545
|
};
|
|
2530
2546
|
}
|
|
2547
|
+
function shiftIsoDay(day, offsetDays) {
|
|
2548
|
+
const parsed = /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
|
|
2549
|
+
parsed.setUTCDate(parsed.getUTCDate() + offsetDays);
|
|
2550
|
+
return parsed.toISOString().slice(0, 10);
|
|
2551
|
+
}
|
|
2552
|
+
function listIsoDayWindow(endDay, days) {
|
|
2553
|
+
return Array.from(
|
|
2554
|
+
{ length: Math.max(0, days) },
|
|
2555
|
+
(_, index) => shiftIsoDay(endDay, index - (days - 1))
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
function getBacklogValueOnOrBeforeDay(snapshots, day) {
|
|
2559
|
+
let currentValue;
|
|
2560
|
+
for (const snapshot of snapshots) {
|
|
2561
|
+
if (snapshot.day > day) {
|
|
2562
|
+
break;
|
|
2563
|
+
}
|
|
2564
|
+
currentValue = snapshot.openIssueCount;
|
|
2565
|
+
}
|
|
2566
|
+
return currentValue;
|
|
2567
|
+
}
|
|
2568
|
+
function buildBacklogHistorySeries(snapshots, endDay, days) {
|
|
2569
|
+
const series = [];
|
|
2570
|
+
for (const day of listIsoDayWindow(endDay, days)) {
|
|
2571
|
+
const value = getBacklogValueOnOrBeforeDay(snapshots, day);
|
|
2572
|
+
if (value === void 0) {
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
series.push({
|
|
2576
|
+
day,
|
|
2577
|
+
value
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
return series;
|
|
2581
|
+
}
|
|
2582
|
+
function getCompanyActivityMetricValue(rollup, metric) {
|
|
2583
|
+
return metric === "githubIssuesClosedCount" ? rollup.githubIssuesClosedCount : rollup.paperclipPullRequestsCreatedCount;
|
|
2584
|
+
}
|
|
2585
|
+
function buildActivityHistorySeries(rollups, metric, endDay, days) {
|
|
2586
|
+
const rollupsByDay = new Map(rollups.map((rollup) => [rollup.day, rollup]));
|
|
2587
|
+
return listIsoDayWindow(endDay, days).map((day) => ({
|
|
2588
|
+
day,
|
|
2589
|
+
value: getCompanyActivityMetricValue(
|
|
2590
|
+
rollupsByDay.get(day) ?? createEmptyCompanyActivityRollup(day, `${day}T00:00:00.000Z`),
|
|
2591
|
+
metric
|
|
2592
|
+
)
|
|
2593
|
+
}));
|
|
2594
|
+
}
|
|
2595
|
+
function sumActivityMetricForWindow(rollups, metric, endDay, days) {
|
|
2596
|
+
return buildActivityHistorySeries(rollups, metric, endDay, days).reduce((sum, point) => sum + point.value, 0);
|
|
2597
|
+
}
|
|
2598
|
+
async function buildDashboardMetricsData(ctx, input) {
|
|
2599
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
2600
|
+
if (!companyId) {
|
|
2601
|
+
return {
|
|
2602
|
+
status: "company_required",
|
|
2603
|
+
historyWindowDays: COMPANY_KPI_CHART_WINDOW_DAYS,
|
|
2604
|
+
comparisonWindowDays: COMPANY_KPI_COMPARISON_WINDOW_DAYS,
|
|
2605
|
+
notes: {
|
|
2606
|
+
backlogHistoryAvailable: false,
|
|
2607
|
+
activityHistoryAvailable: false
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
2612
|
+
const mappings = getSyncableMappingsForScope(settings.mappings, companyId);
|
|
2613
|
+
const companyKpis = normalizeCompanyKpiState(await ctx.state.get(COMPANY_KPI_SCOPE));
|
|
2614
|
+
const backlogSnapshots = getCompanyBacklogSnapshots(companyKpis, companyId);
|
|
2615
|
+
const activityRollups = getCompanyActivityRollups(companyKpis, companyId);
|
|
2616
|
+
const latestBacklogSnapshot = backlogSnapshots.at(-1);
|
|
2617
|
+
const latestActivityRollup = activityRollups.at(-1);
|
|
2618
|
+
const backlogEndDay = latestBacklogSnapshot?.day;
|
|
2619
|
+
const activityEndDay = latestActivityRollup?.day;
|
|
2620
|
+
return {
|
|
2621
|
+
status: mappings.length > 0 ? "ready" : "no_mappings",
|
|
2622
|
+
companyId,
|
|
2623
|
+
mappedRepositoryCount: mappings.length,
|
|
2624
|
+
historyWindowDays: COMPANY_KPI_CHART_WINDOW_DAYS,
|
|
2625
|
+
comparisonWindowDays: COMPANY_KPI_COMPARISON_WINDOW_DAYS,
|
|
2626
|
+
backlog: {
|
|
2627
|
+
...latestBacklogSnapshot ? { lastCapturedAt: latestBacklogSnapshot.capturedAt } : {},
|
|
2628
|
+
...latestBacklogSnapshot ? { currentOpenIssueCount: latestBacklogSnapshot.openIssueCount } : {},
|
|
2629
|
+
...backlogEndDay ? {
|
|
2630
|
+
comparisonOpenIssueCount: getBacklogValueOnOrBeforeDay(
|
|
2631
|
+
backlogSnapshots,
|
|
2632
|
+
shiftIsoDay(backlogEndDay, -(COMPANY_KPI_COMPARISON_WINDOW_DAYS - 1))
|
|
2633
|
+
)
|
|
2634
|
+
} : {},
|
|
2635
|
+
history: backlogEndDay ? buildBacklogHistorySeries(backlogSnapshots, backlogEndDay, COMPANY_KPI_CHART_WINDOW_DAYS) : []
|
|
2636
|
+
},
|
|
2637
|
+
githubIssuesClosed: {
|
|
2638
|
+
...latestActivityRollup ? { lastRecordedAt: latestActivityRollup.updatedAt } : {},
|
|
2639
|
+
currentPeriodCount: activityEndDay ? sumActivityMetricForWindow(
|
|
2640
|
+
activityRollups,
|
|
2641
|
+
"githubIssuesClosedCount",
|
|
2642
|
+
activityEndDay,
|
|
2643
|
+
COMPANY_KPI_COMPARISON_WINDOW_DAYS
|
|
2644
|
+
) : 0,
|
|
2645
|
+
previousPeriodCount: activityEndDay ? sumActivityMetricForWindow(
|
|
2646
|
+
activityRollups,
|
|
2647
|
+
"githubIssuesClosedCount",
|
|
2648
|
+
shiftIsoDay(activityEndDay, -COMPANY_KPI_COMPARISON_WINDOW_DAYS),
|
|
2649
|
+
COMPANY_KPI_COMPARISON_WINDOW_DAYS
|
|
2650
|
+
) : 0,
|
|
2651
|
+
history: activityEndDay ? buildActivityHistorySeries(
|
|
2652
|
+
activityRollups,
|
|
2653
|
+
"githubIssuesClosedCount",
|
|
2654
|
+
activityEndDay,
|
|
2655
|
+
COMPANY_KPI_CHART_WINDOW_DAYS
|
|
2656
|
+
) : []
|
|
2657
|
+
},
|
|
2658
|
+
paperclipPullRequestsCreated: {
|
|
2659
|
+
...latestActivityRollup ? { lastRecordedAt: latestActivityRollup.updatedAt } : {},
|
|
2660
|
+
currentPeriodCount: activityEndDay ? sumActivityMetricForWindow(
|
|
2661
|
+
activityRollups,
|
|
2662
|
+
"paperclipPullRequestsCreatedCount",
|
|
2663
|
+
activityEndDay,
|
|
2664
|
+
COMPANY_KPI_COMPARISON_WINDOW_DAYS
|
|
2665
|
+
) : 0,
|
|
2666
|
+
previousPeriodCount: activityEndDay ? sumActivityMetricForWindow(
|
|
2667
|
+
activityRollups,
|
|
2668
|
+
"paperclipPullRequestsCreatedCount",
|
|
2669
|
+
shiftIsoDay(activityEndDay, -COMPANY_KPI_COMPARISON_WINDOW_DAYS),
|
|
2670
|
+
COMPANY_KPI_COMPARISON_WINDOW_DAYS
|
|
2671
|
+
) : 0,
|
|
2672
|
+
history: activityEndDay ? buildActivityHistorySeries(
|
|
2673
|
+
activityRollups,
|
|
2674
|
+
"paperclipPullRequestsCreatedCount",
|
|
2675
|
+
activityEndDay,
|
|
2676
|
+
COMPANY_KPI_CHART_WINDOW_DAYS
|
|
2677
|
+
) : []
|
|
2678
|
+
},
|
|
2679
|
+
notes: {
|
|
2680
|
+
backlogHistoryAvailable: backlogSnapshots.length > 0,
|
|
2681
|
+
activityHistoryAvailable: activityRollups.length > 0
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2531
2685
|
async function buildIssueGitHubDetails(ctx, input) {
|
|
2532
2686
|
const issueId = typeof input.issueId === "string" && input.issueId.trim() ? input.issueId.trim() : void 0;
|
|
2533
2687
|
const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
|
|
@@ -3605,6 +3759,193 @@ function normalizeSettings(value) {
|
|
|
3605
3759
|
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : void 0
|
|
3606
3760
|
};
|
|
3607
3761
|
}
|
|
3762
|
+
function getIsoDayString(value) {
|
|
3763
|
+
return coerceDate(value).toISOString().slice(0, 10);
|
|
3764
|
+
}
|
|
3765
|
+
function parseDateValue(value) {
|
|
3766
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
3767
|
+
return value;
|
|
3768
|
+
}
|
|
3769
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
3770
|
+
const parsed = new Date(value);
|
|
3771
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
3772
|
+
return parsed;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
function normalizeIsoDayString(value) {
|
|
3778
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
3779
|
+
return void 0;
|
|
3780
|
+
}
|
|
3781
|
+
return parseDateValue(value.trim())?.toISOString().slice(0, 10);
|
|
3782
|
+
}
|
|
3783
|
+
function normalizeIsoTimestampString(value) {
|
|
3784
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
3785
|
+
return void 0;
|
|
3786
|
+
}
|
|
3787
|
+
return parseDateValue(value.trim())?.toISOString();
|
|
3788
|
+
}
|
|
3789
|
+
function normalizeCompanyBacklogSnapshot(value) {
|
|
3790
|
+
if (!value || typeof value !== "object") {
|
|
3791
|
+
return null;
|
|
3792
|
+
}
|
|
3793
|
+
const record = value;
|
|
3794
|
+
const day = normalizeIsoDayString(record.day);
|
|
3795
|
+
const capturedAt = normalizeIsoTimestampString(record.capturedAt);
|
|
3796
|
+
const openIssueCount = typeof record.openIssueCount === "number" && record.openIssueCount >= 0 ? Math.floor(record.openIssueCount) : NaN;
|
|
3797
|
+
const repositoryCount = typeof record.repositoryCount === "number" && record.repositoryCount >= 0 ? Math.floor(record.repositoryCount) : NaN;
|
|
3798
|
+
if (!day || !capturedAt || Number.isNaN(openIssueCount) || Number.isNaN(repositoryCount)) {
|
|
3799
|
+
return null;
|
|
3800
|
+
}
|
|
3801
|
+
return {
|
|
3802
|
+
day,
|
|
3803
|
+
capturedAt,
|
|
3804
|
+
openIssueCount,
|
|
3805
|
+
repositoryCount
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
function normalizeCompanyBacklogSnapshots(value) {
|
|
3809
|
+
if (!Array.isArray(value)) {
|
|
3810
|
+
return [];
|
|
3811
|
+
}
|
|
3812
|
+
const snapshotsByDay = /* @__PURE__ */ new Map();
|
|
3813
|
+
for (const entry of value) {
|
|
3814
|
+
const snapshot = normalizeCompanyBacklogSnapshot(entry);
|
|
3815
|
+
if (!snapshot) {
|
|
3816
|
+
continue;
|
|
3817
|
+
}
|
|
3818
|
+
const existing = snapshotsByDay.get(snapshot.day);
|
|
3819
|
+
if (!existing || existing.capturedAt.localeCompare(snapshot.capturedAt) <= 0) {
|
|
3820
|
+
snapshotsByDay.set(snapshot.day, snapshot);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
return [...snapshotsByDay.values()].sort((left, right) => left.day.localeCompare(right.day)).slice(-MAX_COMPANY_BACKLOG_SNAPSHOTS);
|
|
3824
|
+
}
|
|
3825
|
+
function createEmptyCompanyActivityRollup(day, updatedAt) {
|
|
3826
|
+
return {
|
|
3827
|
+
day,
|
|
3828
|
+
updatedAt,
|
|
3829
|
+
githubIssuesClosedCount: 0,
|
|
3830
|
+
paperclipPullRequestsCreatedCount: 0
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
function normalizeCompanyActivityRollup(value) {
|
|
3834
|
+
if (!value || typeof value !== "object") {
|
|
3835
|
+
return null;
|
|
3836
|
+
}
|
|
3837
|
+
const record = value;
|
|
3838
|
+
const day = normalizeIsoDayString(record.day);
|
|
3839
|
+
const updatedAt = normalizeIsoTimestampString(record.updatedAt);
|
|
3840
|
+
const githubIssuesClosedCount = typeof record.githubIssuesClosedCount === "number" && record.githubIssuesClosedCount >= 0 ? Math.floor(record.githubIssuesClosedCount) : 0;
|
|
3841
|
+
const paperclipPullRequestsCreatedCount = typeof record.paperclipPullRequestsCreatedCount === "number" && record.paperclipPullRequestsCreatedCount >= 0 ? Math.floor(record.paperclipPullRequestsCreatedCount) : 0;
|
|
3842
|
+
const paperclipPullRequestsMergedCount = typeof record.paperclipPullRequestsMergedCount === "number" && record.paperclipPullRequestsMergedCount >= 0 ? Math.floor(record.paperclipPullRequestsMergedCount) : void 0;
|
|
3843
|
+
if (!day || !updatedAt) {
|
|
3844
|
+
return null;
|
|
3845
|
+
}
|
|
3846
|
+
return {
|
|
3847
|
+
day,
|
|
3848
|
+
updatedAt,
|
|
3849
|
+
githubIssuesClosedCount,
|
|
3850
|
+
paperclipPullRequestsCreatedCount,
|
|
3851
|
+
...paperclipPullRequestsMergedCount !== void 0 ? { paperclipPullRequestsMergedCount } : {}
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function normalizeCompanyActivityRollups(value) {
|
|
3855
|
+
if (!Array.isArray(value)) {
|
|
3856
|
+
return [];
|
|
3857
|
+
}
|
|
3858
|
+
const rollupsByDay = /* @__PURE__ */ new Map();
|
|
3859
|
+
for (const entry of value) {
|
|
3860
|
+
const rollup = normalizeCompanyActivityRollup(entry);
|
|
3861
|
+
if (!rollup) {
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
const existing = rollupsByDay.get(rollup.day);
|
|
3865
|
+
if (!existing || existing.updatedAt.localeCompare(rollup.updatedAt) <= 0) {
|
|
3866
|
+
rollupsByDay.set(rollup.day, rollup);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
return [...rollupsByDay.values()].sort((left, right) => left.day.localeCompare(right.day)).slice(-MAX_COMPANY_ACTIVITY_ROLLUPS);
|
|
3870
|
+
}
|
|
3871
|
+
function normalizeCompanyMetricEventKeyRecord(value) {
|
|
3872
|
+
if (!value || typeof value !== "object") {
|
|
3873
|
+
return null;
|
|
3874
|
+
}
|
|
3875
|
+
const record = value;
|
|
3876
|
+
const key = typeof record.key === "string" && record.key.trim() ? record.key.trim() : "";
|
|
3877
|
+
const recordedAt = typeof record.recordedAt === "string" ? coerceDate(record.recordedAt).toISOString() : void 0;
|
|
3878
|
+
if (!key || !recordedAt) {
|
|
3879
|
+
return null;
|
|
3880
|
+
}
|
|
3881
|
+
return {
|
|
3882
|
+
key,
|
|
3883
|
+
recordedAt
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
function normalizeCompanyMetricEventKeyRecords(value) {
|
|
3887
|
+
if (!Array.isArray(value)) {
|
|
3888
|
+
return [];
|
|
3889
|
+
}
|
|
3890
|
+
const entries = value.map((entry) => normalizeCompanyMetricEventKeyRecord(entry)).filter((entry) => entry !== null).sort((left, right) => left.recordedAt.localeCompare(right.recordedAt));
|
|
3891
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3892
|
+
const deduped = [];
|
|
3893
|
+
for (const entry of entries) {
|
|
3894
|
+
if (keys.has(entry.key)) {
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
keys.add(entry.key);
|
|
3898
|
+
deduped.push(entry);
|
|
3899
|
+
}
|
|
3900
|
+
return deduped.slice(-MAX_COMPANY_METRIC_EVENT_KEYS);
|
|
3901
|
+
}
|
|
3902
|
+
function normalizeCompanyBacklogSnapshotsByCompanyId(value) {
|
|
3903
|
+
if (!value || typeof value !== "object") {
|
|
3904
|
+
return void 0;
|
|
3905
|
+
}
|
|
3906
|
+
const entries = Object.entries(value).map(([companyId, snapshots]) => {
|
|
3907
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
3908
|
+
const normalizedSnapshots = normalizeCompanyBacklogSnapshots(snapshots);
|
|
3909
|
+
return normalizedCompanyId && normalizedSnapshots.length > 0 ? [normalizedCompanyId, normalizedSnapshots] : null;
|
|
3910
|
+
}).filter((entry) => entry !== null);
|
|
3911
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3912
|
+
}
|
|
3913
|
+
function normalizeCompanyActivityRollupsByCompanyId(value) {
|
|
3914
|
+
if (!value || typeof value !== "object") {
|
|
3915
|
+
return void 0;
|
|
3916
|
+
}
|
|
3917
|
+
const entries = Object.entries(value).map(([companyId, rollups]) => {
|
|
3918
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
3919
|
+
const normalizedRollups = normalizeCompanyActivityRollups(rollups);
|
|
3920
|
+
return normalizedCompanyId && normalizedRollups.length > 0 ? [normalizedCompanyId, normalizedRollups] : null;
|
|
3921
|
+
}).filter((entry) => entry !== null);
|
|
3922
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3923
|
+
}
|
|
3924
|
+
function normalizeCompanyMetricEventKeysByCompanyId(value) {
|
|
3925
|
+
if (!value || typeof value !== "object") {
|
|
3926
|
+
return void 0;
|
|
3927
|
+
}
|
|
3928
|
+
const entries = Object.entries(value).map(([companyId, records]) => {
|
|
3929
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
3930
|
+
const normalizedRecords = normalizeCompanyMetricEventKeyRecords(records);
|
|
3931
|
+
return normalizedCompanyId && normalizedRecords.length > 0 ? [normalizedCompanyId, normalizedRecords] : null;
|
|
3932
|
+
}).filter((entry) => entry !== null);
|
|
3933
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3934
|
+
}
|
|
3935
|
+
function normalizeCompanyKpiState(value) {
|
|
3936
|
+
if (!value || typeof value !== "object") {
|
|
3937
|
+
return {};
|
|
3938
|
+
}
|
|
3939
|
+
const record = value;
|
|
3940
|
+
const backlogSnapshotsByCompanyId = normalizeCompanyBacklogSnapshotsByCompanyId(record.backlogSnapshotsByCompanyId);
|
|
3941
|
+
const activityRollupsByCompanyId = normalizeCompanyActivityRollupsByCompanyId(record.activityRollupsByCompanyId);
|
|
3942
|
+
const metricEventKeysByCompanyId = normalizeCompanyMetricEventKeysByCompanyId(record.metricEventKeysByCompanyId);
|
|
3943
|
+
return {
|
|
3944
|
+
...backlogSnapshotsByCompanyId ? { backlogSnapshotsByCompanyId } : {},
|
|
3945
|
+
...activityRollupsByCompanyId ? { activityRollupsByCompanyId } : {},
|
|
3946
|
+
...metricEventKeysByCompanyId ? { metricEventKeysByCompanyId } : {}
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3608
3949
|
function getScopedSyncState(settings, companyId) {
|
|
3609
3950
|
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
3610
3951
|
if (!normalizedCompanyId) {
|
|
@@ -3702,6 +4043,149 @@ function getSyncableMappingsForScope(mappings, companyId) {
|
|
|
3702
4043
|
function hasAnyScopedValue(valueByCompanyId) {
|
|
3703
4044
|
return Boolean(valueByCompanyId && Object.keys(valueByCompanyId).length > 0);
|
|
3704
4045
|
}
|
|
4046
|
+
function getCompanyBacklogSnapshots(state, companyId) {
|
|
4047
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4048
|
+
if (!normalizedCompanyId) {
|
|
4049
|
+
return [];
|
|
4050
|
+
}
|
|
4051
|
+
return normalizeCompanyBacklogSnapshots(state.backlogSnapshotsByCompanyId?.[normalizedCompanyId]);
|
|
4052
|
+
}
|
|
4053
|
+
function getCompanyActivityRollups(state, companyId) {
|
|
4054
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4055
|
+
if (!normalizedCompanyId) {
|
|
4056
|
+
return [];
|
|
4057
|
+
}
|
|
4058
|
+
return normalizeCompanyActivityRollups(state.activityRollupsByCompanyId?.[normalizedCompanyId]);
|
|
4059
|
+
}
|
|
4060
|
+
function upsertCompanyBacklogSnapshot(state, companyId, snapshot) {
|
|
4061
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4062
|
+
if (!normalizedCompanyId) {
|
|
4063
|
+
return state;
|
|
4064
|
+
}
|
|
4065
|
+
const nextSnapshots = [
|
|
4066
|
+
...getCompanyBacklogSnapshots(state, normalizedCompanyId).filter((entry) => entry.day !== snapshot.day),
|
|
4067
|
+
snapshot
|
|
4068
|
+
].sort((left, right) => left.day.localeCompare(right.day)).slice(-MAX_COMPANY_BACKLOG_SNAPSHOTS);
|
|
4069
|
+
return {
|
|
4070
|
+
...state,
|
|
4071
|
+
backlogSnapshotsByCompanyId: {
|
|
4072
|
+
...state.backlogSnapshotsByCompanyId ?? {},
|
|
4073
|
+
[normalizedCompanyId]: nextSnapshots
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
}
|
|
4077
|
+
function incrementCompanyActivityRollup(state, params) {
|
|
4078
|
+
const normalizedCompanyId = normalizeCompanyId(params.companyId);
|
|
4079
|
+
if (!normalizedCompanyId) {
|
|
4080
|
+
return state;
|
|
4081
|
+
}
|
|
4082
|
+
const occurredAt = coerceDate(params.occurredAt ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4083
|
+
const day = getIsoDayString(occurredAt);
|
|
4084
|
+
const count = Math.max(1, Math.floor(params.count ?? 1));
|
|
4085
|
+
const nextRollups = [...getCompanyActivityRollups(state, normalizedCompanyId)];
|
|
4086
|
+
const existingIndex = nextRollups.findIndex((entry) => entry.day === day);
|
|
4087
|
+
const existing = existingIndex >= 0 ? nextRollups[existingIndex] : createEmptyCompanyActivityRollup(day, occurredAt);
|
|
4088
|
+
const updated = {
|
|
4089
|
+
...existing,
|
|
4090
|
+
updatedAt: occurredAt,
|
|
4091
|
+
[params.metric]: existing[params.metric] + count
|
|
4092
|
+
};
|
|
4093
|
+
if (existingIndex >= 0) {
|
|
4094
|
+
nextRollups.splice(existingIndex, 1, updated);
|
|
4095
|
+
} else {
|
|
4096
|
+
nextRollups.push(updated);
|
|
4097
|
+
}
|
|
4098
|
+
nextRollups.sort((left, right) => left.day.localeCompare(right.day));
|
|
4099
|
+
return {
|
|
4100
|
+
...state,
|
|
4101
|
+
activityRollupsByCompanyId: {
|
|
4102
|
+
...state.activityRollupsByCompanyId ?? {},
|
|
4103
|
+
[normalizedCompanyId]: nextRollups.slice(-MAX_COMPANY_ACTIVITY_ROLLUPS)
|
|
4104
|
+
}
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
function hasRecordedCompanyMetricEventKey(state, companyId, eventKey) {
|
|
4108
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4109
|
+
if (!normalizedCompanyId || !eventKey.trim()) {
|
|
4110
|
+
return false;
|
|
4111
|
+
}
|
|
4112
|
+
return (state.metricEventKeysByCompanyId?.[normalizedCompanyId] ?? []).some((entry) => entry.key === eventKey);
|
|
4113
|
+
}
|
|
4114
|
+
function rememberCompanyMetricEventKey(state, companyId, eventKey, recordedAt) {
|
|
4115
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4116
|
+
const trimmedKey = eventKey.trim();
|
|
4117
|
+
if (!normalizedCompanyId || !trimmedKey) {
|
|
4118
|
+
return state;
|
|
4119
|
+
}
|
|
4120
|
+
const nextRecords = [
|
|
4121
|
+
...(state.metricEventKeysByCompanyId?.[normalizedCompanyId] ?? []).filter((entry) => entry.key !== trimmedKey),
|
|
4122
|
+
{
|
|
4123
|
+
key: trimmedKey,
|
|
4124
|
+
recordedAt
|
|
4125
|
+
}
|
|
4126
|
+
].sort((left, right) => left.recordedAt.localeCompare(right.recordedAt)).slice(-MAX_COMPANY_METRIC_EVENT_KEYS);
|
|
4127
|
+
return {
|
|
4128
|
+
...state,
|
|
4129
|
+
metricEventKeysByCompanyId: {
|
|
4130
|
+
...state.metricEventKeysByCompanyId ?? {},
|
|
4131
|
+
[normalizedCompanyId]: nextRecords
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
function buildCompanyMetricEventKey(params) {
|
|
4136
|
+
const pullRequestUrl = normalizeGitHubPullRequestHtmlUrl(params.pullRequestUrl);
|
|
4137
|
+
if (pullRequestUrl) {
|
|
4138
|
+
return `${params.metric}:${pullRequestUrl}`;
|
|
4139
|
+
}
|
|
4140
|
+
const repository = typeof params.repositoryUrl === "string" ? parseRepositoryReference(params.repositoryUrl) : null;
|
|
4141
|
+
const pullRequestNumber = typeof params.pullRequestNumber === "number" && params.pullRequestNumber >= 1 ? Math.floor(params.pullRequestNumber) : void 0;
|
|
4142
|
+
if (repository && pullRequestNumber) {
|
|
4143
|
+
return `${params.metric}:${repository.url}/pull/${pullRequestNumber}`;
|
|
4144
|
+
}
|
|
4145
|
+
const explicitEventKey = normalizeOptionalString2(params.eventKey);
|
|
4146
|
+
if (explicitEventKey) {
|
|
4147
|
+
return `${params.metric}:${explicitEventKey}`;
|
|
4148
|
+
}
|
|
4149
|
+
return void 0;
|
|
4150
|
+
}
|
|
4151
|
+
function recordCompanyActivityMetricEvent(state, params) {
|
|
4152
|
+
const normalizedCompanyId = normalizeCompanyId(params.companyId);
|
|
4153
|
+
if (!normalizedCompanyId) {
|
|
4154
|
+
return {
|
|
4155
|
+
state,
|
|
4156
|
+
recorded: false
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
const occurredAt = coerceDate(params.occurredAt ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4160
|
+
const eventKey = buildCompanyMetricEventKey({
|
|
4161
|
+
metric: params.metric,
|
|
4162
|
+
eventKey: params.eventKey,
|
|
4163
|
+
repositoryUrl: params.repositoryUrl,
|
|
4164
|
+
pullRequestNumber: params.pullRequestNumber,
|
|
4165
|
+
pullRequestUrl: params.pullRequestUrl
|
|
4166
|
+
});
|
|
4167
|
+
if (eventKey && hasRecordedCompanyMetricEventKey(state, normalizedCompanyId, eventKey)) {
|
|
4168
|
+
return {
|
|
4169
|
+
state,
|
|
4170
|
+
recorded: false,
|
|
4171
|
+
eventKey
|
|
4172
|
+
};
|
|
4173
|
+
}
|
|
4174
|
+
let nextState = incrementCompanyActivityRollup(state, {
|
|
4175
|
+
companyId: normalizedCompanyId,
|
|
4176
|
+
metric: params.metric,
|
|
4177
|
+
count: params.count,
|
|
4178
|
+
occurredAt
|
|
4179
|
+
});
|
|
4180
|
+
if (eventKey) {
|
|
4181
|
+
nextState = rememberCompanyMetricEventKey(nextState, normalizedCompanyId, eventKey, occurredAt);
|
|
4182
|
+
}
|
|
4183
|
+
return {
|
|
4184
|
+
state: nextState,
|
|
4185
|
+
recorded: true,
|
|
4186
|
+
...eventKey ? { eventKey } : {}
|
|
4187
|
+
};
|
|
4188
|
+
}
|
|
3705
4189
|
function normalizeImportRegistry(value) {
|
|
3706
4190
|
if (!Array.isArray(value)) {
|
|
3707
4191
|
return [];
|
|
@@ -3722,6 +4206,7 @@ function normalizeImportRegistry(value) {
|
|
|
3722
4206
|
const paperclipProjectId = typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? record.paperclipProjectId.trim() : void 0;
|
|
3723
4207
|
const companyId = typeof record.companyId === "string" && record.companyId.trim() ? record.companyId.trim() : void 0;
|
|
3724
4208
|
const lastSeenCommentCount = typeof record.lastSeenCommentCount === "number" && record.lastSeenCommentCount >= 0 ? Math.floor(record.lastSeenCommentCount) : void 0;
|
|
4209
|
+
const lastSeenGitHubState = record.lastSeenGitHubState === "open" || record.lastSeenGitHubState === "closed" ? record.lastSeenGitHubState : void 0;
|
|
3725
4210
|
const linkedPullRequestCommentCounts = normalizeGitHubPullRequestCommentCountRecords(
|
|
3726
4211
|
record.linkedPullRequestCommentCounts
|
|
3727
4212
|
);
|
|
@@ -3738,6 +4223,7 @@ function normalizeImportRegistry(value) {
|
|
|
3738
4223
|
...paperclipProjectId ? { paperclipProjectId } : {},
|
|
3739
4224
|
...companyId ? { companyId } : {},
|
|
3740
4225
|
...lastSeenCommentCount !== void 0 ? { lastSeenCommentCount } : {},
|
|
4226
|
+
...lastSeenGitHubState ? { lastSeenGitHubState } : {},
|
|
3741
4227
|
...linkedPullRequestCommentCounts.length > 0 ? { linkedPullRequestCommentCounts } : {}
|
|
3742
4228
|
};
|
|
3743
4229
|
}).filter((entry) => entry !== null);
|
|
@@ -3776,6 +4262,12 @@ function countImportedIssuesForMappings(importRegistry, mappings) {
|
|
|
3776
4262
|
function buildTrackedIssueProgressKey(mapping, githubIssueId) {
|
|
3777
4263
|
return `${mapping.id}:${githubIssueId}`;
|
|
3778
4264
|
}
|
|
4265
|
+
function buildTrackedPullRequestIssueProgressKey(mapping, record) {
|
|
4266
|
+
return `${mapping.id}:pull-request:${record.paperclipIssueId}:${buildGitHubPullRequestReferenceKey({
|
|
4267
|
+
number: record.data.githubPullRequestNumber,
|
|
4268
|
+
repositoryUrl: record.data.repositoryUrl
|
|
4269
|
+
})}`;
|
|
4270
|
+
}
|
|
3779
4271
|
function buildImportedIssueRecord(mapping, issue, paperclipIssueId, importedAt) {
|
|
3780
4272
|
return {
|
|
3781
4273
|
mappingId: mapping.id,
|
|
@@ -3784,6 +4276,7 @@ function buildImportedIssueRecord(mapping, issue, paperclipIssueId, importedAt)
|
|
|
3784
4276
|
paperclipIssueId,
|
|
3785
4277
|
importedAt,
|
|
3786
4278
|
lastSeenCommentCount: issue.commentsCount,
|
|
4279
|
+
lastSeenGitHubState: issue.state,
|
|
3787
4280
|
linkedPullRequestCommentCounts: [],
|
|
3788
4281
|
repositoryUrl: getNormalizedMappingRepositoryUrl(mapping),
|
|
3789
4282
|
paperclipProjectId: mapping.paperclipProjectId,
|
|
@@ -3794,6 +4287,7 @@ function refreshImportedIssueRecordForMapping(record, mapping, issue) {
|
|
|
3794
4287
|
record.mappingId = mapping.id;
|
|
3795
4288
|
record.githubIssueNumber = issue.number;
|
|
3796
4289
|
record.lastSeenCommentCount ??= issue.commentsCount;
|
|
4290
|
+
record.lastSeenGitHubState ??= issue.state;
|
|
3797
4291
|
record.repositoryUrl = getNormalizedMappingRepositoryUrl(mapping);
|
|
3798
4292
|
record.paperclipProjectId = mapping.paperclipProjectId;
|
|
3799
4293
|
record.companyId = mapping.companyId;
|
|
@@ -4616,6 +5110,27 @@ function buildSyncFallbackExecutionStatePatch(params) {
|
|
|
4616
5110
|
}
|
|
4617
5111
|
return void 0;
|
|
4618
5112
|
}
|
|
5113
|
+
function describeGitHubLinkedPullRequestsStatusReason(linkedPullRequests) {
|
|
5114
|
+
const linkedPullRequestSubject = linkedPullRequests.length === 1 ? "the linked pull request" : "linked pull requests";
|
|
5115
|
+
const linkedPullRequestVerb = linkedPullRequests.length === 1 ? "has" : "have";
|
|
5116
|
+
const blockingConditions = [...new Set(
|
|
5117
|
+
linkedPullRequests.flatMap((pullRequest) => listGitHubPullRequestSyncBlockingConditions(pullRequest))
|
|
5118
|
+
)];
|
|
5119
|
+
const hasUnfinishedCi = linkedPullRequests.some((pullRequest) => pullRequest.ciState === "unfinished");
|
|
5120
|
+
const hasUnknownMergeability = linkedPullRequests.some(
|
|
5121
|
+
(pullRequest) => pullRequest.mergeStateStatus === "unknown"
|
|
5122
|
+
);
|
|
5123
|
+
if (blockingConditions.length > 0) {
|
|
5124
|
+
return `${linkedPullRequestSubject} ${linkedPullRequestVerb} ${formatPlainTextList(blockingConditions)}`;
|
|
5125
|
+
}
|
|
5126
|
+
if (hasUnfinishedCi) {
|
|
5127
|
+
return `${linkedPullRequestSubject} still ${linkedPullRequestVerb} unfinished CI jobs`;
|
|
5128
|
+
}
|
|
5129
|
+
if (hasUnknownMergeability) {
|
|
5130
|
+
return `${linkedPullRequestSubject} ${linkedPullRequestVerb} unknown mergeability`;
|
|
5131
|
+
}
|
|
5132
|
+
return `${linkedPullRequestSubject} ${linkedPullRequestVerb} green CI with all review threads resolved`;
|
|
5133
|
+
}
|
|
4619
5134
|
function describeGitHubStatusTransitionReason(params) {
|
|
4620
5135
|
const { snapshot, hasTrustedNewComment, maintainerAuthoredImportedIssue } = params;
|
|
4621
5136
|
if (snapshot.state === "closed") {
|
|
@@ -4637,25 +5152,7 @@ function describeGitHubStatusTransitionReason(params) {
|
|
|
4637
5152
|
}
|
|
4638
5153
|
return "the GitHub issue is open with no linked pull requests";
|
|
4639
5154
|
}
|
|
4640
|
-
|
|
4641
|
-
const linkedPullRequestVerb = snapshot.linkedPullRequests.length === 1 ? "has" : "have";
|
|
4642
|
-
const blockingConditions = [...new Set(
|
|
4643
|
-
snapshot.linkedPullRequests.flatMap((pullRequest) => listGitHubPullRequestSyncBlockingConditions(pullRequest))
|
|
4644
|
-
)];
|
|
4645
|
-
const hasUnfinishedCi = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.ciState === "unfinished");
|
|
4646
|
-
const hasUnknownMergeability = snapshot.linkedPullRequests.some(
|
|
4647
|
-
(pullRequest) => pullRequest.mergeStateStatus === "unknown"
|
|
4648
|
-
);
|
|
4649
|
-
if (blockingConditions.length > 0) {
|
|
4650
|
-
return `${linkedPullRequestSubject} ${linkedPullRequestVerb} ${formatPlainTextList(blockingConditions)}`;
|
|
4651
|
-
}
|
|
4652
|
-
if (hasUnfinishedCi) {
|
|
4653
|
-
return `${linkedPullRequestSubject} still ${linkedPullRequestVerb} unfinished CI jobs`;
|
|
4654
|
-
}
|
|
4655
|
-
if (hasUnknownMergeability) {
|
|
4656
|
-
return `${linkedPullRequestSubject} ${linkedPullRequestVerb} unknown mergeability`;
|
|
4657
|
-
}
|
|
4658
|
-
return `${linkedPullRequestSubject} ${linkedPullRequestVerb} green CI with all review threads resolved`;
|
|
5155
|
+
return describeGitHubLinkedPullRequestsStatusReason(snapshot.linkedPullRequests);
|
|
4659
5156
|
}
|
|
4660
5157
|
function buildStatusTransitionCommentAnnotation(params) {
|
|
4661
5158
|
const { repository, snapshot, previousStatus, nextStatus, reason } = params;
|
|
@@ -4698,6 +5195,10 @@ function buildPaperclipIssueStatusTransitionComment(params) {
|
|
|
4698
5195
|
})
|
|
4699
5196
|
};
|
|
4700
5197
|
}
|
|
5198
|
+
function buildPaperclipPullRequestIssueStatusTransitionComment(params) {
|
|
5199
|
+
const reason = describeGitHubLinkedPullRequestsStatusReason([params.pullRequest]);
|
|
5200
|
+
return `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(params.previousStatus)}\` to \`${formatPaperclipIssueStatus(params.nextStatus)}\` because ${reason}.`;
|
|
5201
|
+
}
|
|
4701
5202
|
function resolvePaperclipIssueStatus(params) {
|
|
4702
5203
|
const {
|
|
4703
5204
|
currentStatus,
|
|
@@ -4730,6 +5231,15 @@ function resolvePaperclipIssueStatus(params) {
|
|
|
4730
5231
|
}
|
|
4731
5232
|
return currentStatus;
|
|
4732
5233
|
}
|
|
5234
|
+
function resolvePaperclipPullRequestIssueStatus(params) {
|
|
5235
|
+
const { currentStatus, pullRequest, hasExecutorHandoffTarget } = params;
|
|
5236
|
+
if (currentStatus === "done" || currentStatus === "cancelled") {
|
|
5237
|
+
return currentStatus;
|
|
5238
|
+
}
|
|
5239
|
+
return resolvePaperclipStatusFromLinkedPullRequests([pullRequest], {
|
|
5240
|
+
preferInProgress: hasExecutorHandoffTarget
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
4733
5243
|
async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
|
|
4734
5244
|
const linkedPullRequests = [];
|
|
4735
5245
|
const seenPullRequestKeys = /* @__PURE__ */ new Set();
|
|
@@ -5495,6 +6005,24 @@ function parseGitHubIssueHtmlUrl(value) {
|
|
|
5495
6005
|
function normalizeGitHubIssueHtmlUrl(value) {
|
|
5496
6006
|
return parseGitHubIssueHtmlUrl(value)?.issueUrl;
|
|
5497
6007
|
}
|
|
6008
|
+
function normalizeGitHubPullRequestHtmlUrl(value) {
|
|
6009
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
6010
|
+
return void 0;
|
|
6011
|
+
}
|
|
6012
|
+
try {
|
|
6013
|
+
const parsed = new URL(value);
|
|
6014
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "github.com") {
|
|
6015
|
+
return void 0;
|
|
6016
|
+
}
|
|
6017
|
+
const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
|
|
6018
|
+
if (!match) {
|
|
6019
|
+
return void 0;
|
|
6020
|
+
}
|
|
6021
|
+
return `https://github.com/${match[1]}/${match[2]}/pull/${match[3]}`;
|
|
6022
|
+
} catch {
|
|
6023
|
+
return void 0;
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
5498
6026
|
function escapeRegExp(value) {
|
|
5499
6027
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5500
6028
|
}
|
|
@@ -6025,20 +6553,66 @@ async function listGitHubPullRequestLinkRecords(ctx, query = {}) {
|
|
|
6025
6553
|
}
|
|
6026
6554
|
return records;
|
|
6027
6555
|
}
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6556
|
+
function doesGitHubPullRequestLinkRecordMatchMapping(record, mapping) {
|
|
6557
|
+
if (record.data.repositoryUrl !== getNormalizedMappingRepositoryUrl(mapping)) {
|
|
6558
|
+
return false;
|
|
6559
|
+
}
|
|
6560
|
+
if (record.data.companyId && record.data.companyId !== mapping.companyId) {
|
|
6561
|
+
return false;
|
|
6562
|
+
}
|
|
6563
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== mapping.paperclipProjectId) {
|
|
6564
|
+
return false;
|
|
6565
|
+
}
|
|
6566
|
+
return Boolean(mapping.companyId && mapping.paperclipProjectId);
|
|
6567
|
+
}
|
|
6568
|
+
function doesGitHubPullRequestLinkRecordMatchTarget(record, target) {
|
|
6569
|
+
if (!target) {
|
|
6570
|
+
return true;
|
|
6571
|
+
}
|
|
6572
|
+
switch (target.kind) {
|
|
6573
|
+
case "company":
|
|
6574
|
+
return !record.data.companyId || record.data.companyId === target.companyId;
|
|
6575
|
+
case "project":
|
|
6576
|
+
return (!record.data.companyId || record.data.companyId === target.companyId) && (!record.data.paperclipProjectId || record.data.paperclipProjectId === target.projectId);
|
|
6577
|
+
case "issue":
|
|
6578
|
+
return Boolean(target.issueId && record.paperclipIssueId === target.issueId);
|
|
6579
|
+
default:
|
|
6580
|
+
return true;
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
async function listGitHubPullRequestIssueLinksForMapping(ctx, mapping, target) {
|
|
6584
|
+
const records = await listGitHubPullRequestLinkRecords(ctx, {
|
|
6585
|
+
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
6586
|
+
});
|
|
6587
|
+
const recordsByKey = /* @__PURE__ */ new Map();
|
|
6588
|
+
for (const record of records) {
|
|
6589
|
+
if (!doesGitHubPullRequestLinkRecordMatchMapping(record, mapping) || !doesGitHubPullRequestLinkRecordMatchTarget(record, target)) {
|
|
6590
|
+
continue;
|
|
6591
|
+
}
|
|
6592
|
+
recordsByKey.set(
|
|
6593
|
+
`${record.paperclipIssueId}:${buildGitHubPullRequestReferenceKey({
|
|
6594
|
+
number: record.data.githubPullRequestNumber,
|
|
6595
|
+
repositoryUrl: record.data.repositoryUrl
|
|
6596
|
+
})}`,
|
|
6597
|
+
record
|
|
6598
|
+
);
|
|
6599
|
+
}
|
|
6600
|
+
return [...recordsByKey.values()];
|
|
6601
|
+
}
|
|
6602
|
+
async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
|
|
6603
|
+
const issueId = params.issueId.trim();
|
|
6604
|
+
const commentId = params.commentId.trim();
|
|
6605
|
+
for (let offset = 0; ; ) {
|
|
6606
|
+
const page = await ctx.entities.list({
|
|
6607
|
+
entityType: COMMENT_ANNOTATION_ENTITY_TYPE,
|
|
6608
|
+
scopeKind: "issue",
|
|
6609
|
+
scopeId: issueId,
|
|
6610
|
+
externalId: commentId,
|
|
6611
|
+
limit: PAPERCLIP_LABEL_PAGE_SIZE,
|
|
6612
|
+
offset
|
|
6613
|
+
});
|
|
6614
|
+
if (page.length === 0) {
|
|
6615
|
+
break;
|
|
6042
6616
|
}
|
|
6043
6617
|
const match = page.find((entry) => {
|
|
6044
6618
|
if (entry.scopeKind !== "issue" || entry.scopeId !== issueId) {
|
|
@@ -6137,16 +6711,7 @@ async function upsertStatusTransitionCommentAnnotation(ctx, params) {
|
|
|
6137
6711
|
});
|
|
6138
6712
|
}
|
|
6139
6713
|
function coerceDate(value) {
|
|
6140
|
-
|
|
6141
|
-
return value;
|
|
6142
|
-
}
|
|
6143
|
-
if (typeof value === "string" || typeof value === "number") {
|
|
6144
|
-
const parsed = new Date(value);
|
|
6145
|
-
if (!Number.isNaN(parsed.getTime())) {
|
|
6146
|
-
return parsed;
|
|
6147
|
-
}
|
|
6148
|
-
}
|
|
6149
|
-
return /* @__PURE__ */ new Date();
|
|
6714
|
+
return parseDateValue(value) ?? /* @__PURE__ */ new Date();
|
|
6150
6715
|
}
|
|
6151
6716
|
function parsePaperclipIssueLabel(value, expectedCompanyId) {
|
|
6152
6717
|
if (!value || typeof value !== "object") {
|
|
@@ -6208,6 +6773,9 @@ function getPaperclipIssueEndpoint(baseUrl, issueId) {
|
|
|
6208
6773
|
function getPaperclipHealthEndpoint(baseUrl) {
|
|
6209
6774
|
return new URL("/api/health", baseUrl).toString();
|
|
6210
6775
|
}
|
|
6776
|
+
function getPaperclipCurrentAgentEndpoint(baseUrl) {
|
|
6777
|
+
return new URL("/api/agents/me", baseUrl).toString();
|
|
6778
|
+
}
|
|
6211
6779
|
function getPaperclipAgentWakeupEndpoint(baseUrl, agentId) {
|
|
6212
6780
|
return new URL(`/api/agents/${agentId}/wakeup`, baseUrl).toString();
|
|
6213
6781
|
}
|
|
@@ -7342,7 +7910,7 @@ async function ensurePaperclipIssueImported(ctx, mapping, advancedSettings, issu
|
|
|
7342
7910
|
}
|
|
7343
7911
|
return createdIssue.id;
|
|
7344
7912
|
}
|
|
7345
|
-
async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
7913
|
+
async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, assertNotCancelled, onGitHubIssueClosed, onProgress) {
|
|
7346
7914
|
if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
7347
7915
|
return {
|
|
7348
7916
|
updatedStatusesCount: 0,
|
|
@@ -7423,6 +7991,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
7423
7991
|
pullRequestStatusCache
|
|
7424
7992
|
);
|
|
7425
7993
|
const snapshotLinkedPullRequestNumbers = snapshot?.linkedPullRequests.map((pullRequest) => pullRequest.number) ?? [];
|
|
7994
|
+
const previousGitHubState = importedIssue.lastSeenGitHubState;
|
|
7426
7995
|
if (!doIssueNumberListsMatch(snapshotLinkedPullRequestNumbers, warmedLinkedPullRequestNumbers)) {
|
|
7427
7996
|
updateSyncFailureContext(syncFailureContext, {
|
|
7428
7997
|
phase: "syncing_description",
|
|
@@ -7462,6 +8031,15 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
7462
8031
|
},
|
|
7463
8032
|
snapshot.linkedPullRequests
|
|
7464
8033
|
);
|
|
8034
|
+
if (previousGitHubState === "open" && snapshot.state === "closed" && typeof onGitHubIssueClosed === "function") {
|
|
8035
|
+
await onGitHubIssueClosed({
|
|
8036
|
+
companyId: mapping.companyId,
|
|
8037
|
+
githubIssueId: githubIssue.id,
|
|
8038
|
+
githubIssueNumber: githubIssue.number,
|
|
8039
|
+
repositoryUrl: repository.url,
|
|
8040
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8041
|
+
});
|
|
8042
|
+
}
|
|
7465
8043
|
const previousCommentCount = importedIssue.lastSeenCommentCount;
|
|
7466
8044
|
const hasNewIssueComments = snapshot.commentCount > (previousCommentCount ?? snapshot.commentCount);
|
|
7467
8045
|
const hasTrustedNewIssueComment = paperclipIssue.status === "backlog" || !hasNewIssueComments ? false : await hasTrustedNewGitHubIssueComment({
|
|
@@ -7526,6 +8104,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
7526
8104
|
const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
|
|
7527
8105
|
importedIssue.githubIssueNumber = githubIssue.number;
|
|
7528
8106
|
importedIssue.lastSeenCommentCount = snapshot.commentCount;
|
|
8107
|
+
importedIssue.lastSeenGitHubState = snapshot.state;
|
|
7529
8108
|
importedIssue.linkedPullRequestCommentCounts = currentLinkedPullRequestCommentCounts;
|
|
7530
8109
|
if (paperclipIssue.status === nextStatus) {
|
|
7531
8110
|
if (shouldClearTransitionAssignee) {
|
|
@@ -7629,6 +8208,171 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
7629
8208
|
updatedDescriptionsCount
|
|
7630
8209
|
};
|
|
7631
8210
|
}
|
|
8211
|
+
async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
8212
|
+
if (!mapping.companyId || !mapping.paperclipProjectId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
8213
|
+
return {
|
|
8214
|
+
updatedStatusesCount: 0
|
|
8215
|
+
};
|
|
8216
|
+
}
|
|
8217
|
+
let updatedStatusesCount = 0;
|
|
8218
|
+
let completedIssueCount = 0;
|
|
8219
|
+
const mappingCompanyId = mapping.companyId;
|
|
8220
|
+
const mappingProjectId = mapping.paperclipProjectId;
|
|
8221
|
+
const totalIssueCount = pullRequestLinks.length;
|
|
8222
|
+
const queuedIssueWakeups = [];
|
|
8223
|
+
for (const pullRequestLink of pullRequestLinks) {
|
|
8224
|
+
if (assertNotCancelled) {
|
|
8225
|
+
await assertNotCancelled();
|
|
8226
|
+
}
|
|
8227
|
+
try {
|
|
8228
|
+
const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
|
|
8229
|
+
updateSyncFailureContext(syncFailureContext, {
|
|
8230
|
+
phase: "evaluating_github_status",
|
|
8231
|
+
repositoryUrl: pullRequestRepository.url,
|
|
8232
|
+
githubIssueNumber: void 0
|
|
8233
|
+
});
|
|
8234
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8235
|
+
owner: pullRequestRepository.owner,
|
|
8236
|
+
repo: pullRequestRepository.repo,
|
|
8237
|
+
pull_number: pullRequestLink.data.githubPullRequestNumber,
|
|
8238
|
+
headers: {
|
|
8239
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8240
|
+
}
|
|
8241
|
+
});
|
|
8242
|
+
const livePullRequestState = getPullRequestApiState({
|
|
8243
|
+
state: pullRequestResponse.data.state,
|
|
8244
|
+
merged: pullRequestResponse.data.merged
|
|
8245
|
+
}) === "open" ? "open" : "closed";
|
|
8246
|
+
if (livePullRequestState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
|
|
8247
|
+
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
8248
|
+
companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
|
|
8249
|
+
projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
|
|
8250
|
+
issueId: pullRequestLink.paperclipIssueId,
|
|
8251
|
+
repositoryUrl: pullRequestRepository.url,
|
|
8252
|
+
pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
|
|
8253
|
+
pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
|
|
8254
|
+
pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
|
|
8255
|
+
pullRequestState: livePullRequestState
|
|
8256
|
+
});
|
|
8257
|
+
}
|
|
8258
|
+
if (livePullRequestState !== "open") {
|
|
8259
|
+
continue;
|
|
8260
|
+
}
|
|
8261
|
+
const pullRequest = await getGitHubPullRequestStatusSnapshot(
|
|
8262
|
+
octokit,
|
|
8263
|
+
pullRequestRepository,
|
|
8264
|
+
pullRequestLink.data.githubPullRequestNumber,
|
|
8265
|
+
pullRequestStatusCache
|
|
8266
|
+
);
|
|
8267
|
+
const paperclipIssue = await ctx.issues.get(pullRequestLink.paperclipIssueId, mapping.companyId);
|
|
8268
|
+
if (!paperclipIssue) {
|
|
8269
|
+
continue;
|
|
8270
|
+
}
|
|
8271
|
+
const paperclipIssueSyncContext = getPaperclipIssueSyncContext(paperclipIssue);
|
|
8272
|
+
const executorTransitionAssignee = resolvePaperclipIssueExecutorAssignee(
|
|
8273
|
+
paperclipIssueSyncContext,
|
|
8274
|
+
advancedSettings
|
|
8275
|
+
);
|
|
8276
|
+
const nextStatus = resolvePaperclipPullRequestIssueStatus({
|
|
8277
|
+
currentStatus: paperclipIssue.status,
|
|
8278
|
+
pullRequest,
|
|
8279
|
+
hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
|
|
8280
|
+
});
|
|
8281
|
+
const nextTransitionAssignee = resolveSyncTransitionAssignee({
|
|
8282
|
+
nextStatus,
|
|
8283
|
+
syncContext: paperclipIssueSyncContext,
|
|
8284
|
+
advancedSettings
|
|
8285
|
+
});
|
|
8286
|
+
const shouldClearTransitionAssignee = nextStatus === "in_review" && nextTransitionAssignee === null && paperclipIssueSyncContext.assignee !== null;
|
|
8287
|
+
const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
|
|
8288
|
+
const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
|
|
8289
|
+
if (paperclipIssue.status === nextStatus) {
|
|
8290
|
+
if (shouldClearTransitionAssignee) {
|
|
8291
|
+
updateSyncFailureContext(syncFailureContext, {
|
|
8292
|
+
phase: "updating_paperclip_status",
|
|
8293
|
+
repositoryUrl: pullRequestRepository.url,
|
|
8294
|
+
githubIssueNumber: void 0
|
|
8295
|
+
});
|
|
8296
|
+
await updatePaperclipIssueState(ctx, {
|
|
8297
|
+
companyId: mapping.companyId,
|
|
8298
|
+
issueId: pullRequestLink.paperclipIssueId,
|
|
8299
|
+
currentStatus: paperclipIssue.status,
|
|
8300
|
+
syncContext: paperclipIssueSyncContext,
|
|
8301
|
+
nextStatus,
|
|
8302
|
+
clearAssignee: true,
|
|
8303
|
+
transitionComment: "",
|
|
8304
|
+
paperclipApiBaseUrl
|
|
8305
|
+
});
|
|
8306
|
+
}
|
|
8307
|
+
continue;
|
|
8308
|
+
}
|
|
8309
|
+
const transitionComment = buildPaperclipPullRequestIssueStatusTransitionComment({
|
|
8310
|
+
previousStatus: paperclipIssue.status,
|
|
8311
|
+
nextStatus,
|
|
8312
|
+
pullRequest
|
|
8313
|
+
});
|
|
8314
|
+
updateSyncFailureContext(syncFailureContext, {
|
|
8315
|
+
phase: "updating_paperclip_status",
|
|
8316
|
+
repositoryUrl: pullRequestRepository.url,
|
|
8317
|
+
githubIssueNumber: void 0
|
|
8318
|
+
});
|
|
8319
|
+
await updatePaperclipIssueState(ctx, {
|
|
8320
|
+
companyId: mapping.companyId,
|
|
8321
|
+
issueId: pullRequestLink.paperclipIssueId,
|
|
8322
|
+
currentStatus: paperclipIssue.status,
|
|
8323
|
+
syncContext: paperclipIssueSyncContext,
|
|
8324
|
+
nextStatus,
|
|
8325
|
+
...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
|
|
8326
|
+
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
8327
|
+
transitionComment,
|
|
8328
|
+
paperclipApiBaseUrl
|
|
8329
|
+
});
|
|
8330
|
+
updatedStatusesCount += 1;
|
|
8331
|
+
if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
|
|
8332
|
+
queuedIssueWakeups.push({
|
|
8333
|
+
assigneeAgentId: nextTransitionAssignee.principal.id,
|
|
8334
|
+
paperclipIssueId: pullRequestLink.paperclipIssueId,
|
|
8335
|
+
reason: STATUS_TRANSITION_WAKE_REASON,
|
|
8336
|
+
mutation: "status_transition",
|
|
8337
|
+
previousStatus: paperclipIssue.status,
|
|
8338
|
+
nextStatus
|
|
8339
|
+
});
|
|
8340
|
+
}
|
|
8341
|
+
} catch (error) {
|
|
8342
|
+
if (isGitHubRateLimitError(error)) {
|
|
8343
|
+
throw error;
|
|
8344
|
+
}
|
|
8345
|
+
recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
|
|
8346
|
+
continue;
|
|
8347
|
+
} finally {
|
|
8348
|
+
completedIssueCount += 1;
|
|
8349
|
+
if (onProgress) {
|
|
8350
|
+
await onProgress({
|
|
8351
|
+
pullRequestLink,
|
|
8352
|
+
completedIssueCount,
|
|
8353
|
+
totalIssueCount
|
|
8354
|
+
});
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
}
|
|
8358
|
+
await mapWithConcurrency(
|
|
8359
|
+
queuedIssueWakeups,
|
|
8360
|
+
IMPORTED_ISSUE_WAKEUP_CONCURRENCY,
|
|
8361
|
+
async (queuedWakeup) => wakePaperclipIssueAssignee(ctx, {
|
|
8362
|
+
assigneeAgentId: queuedWakeup.assigneeAgentId,
|
|
8363
|
+
paperclipIssueId: queuedWakeup.paperclipIssueId,
|
|
8364
|
+
companyId: mapping.companyId,
|
|
8365
|
+
paperclipApiBaseUrl,
|
|
8366
|
+
reason: queuedWakeup.reason,
|
|
8367
|
+
mutation: queuedWakeup.mutation,
|
|
8368
|
+
previousStatus: queuedWakeup.previousStatus,
|
|
8369
|
+
nextStatus: queuedWakeup.nextStatus
|
|
8370
|
+
})
|
|
8371
|
+
);
|
|
8372
|
+
return {
|
|
8373
|
+
updatedStatusesCount
|
|
8374
|
+
};
|
|
8375
|
+
}
|
|
7632
8376
|
async function getResolvedConfig(ctx) {
|
|
7633
8377
|
const [savedConfig, externalConfig] = await Promise.all([
|
|
7634
8378
|
ctx.config.get(),
|
|
@@ -7827,6 +8571,259 @@ async function executeGitHubTool(fn) {
|
|
|
7827
8571
|
return buildToolErrorResult(error);
|
|
7828
8572
|
}
|
|
7829
8573
|
}
|
|
8574
|
+
function normalizeCompanyActivityMetricInputValue(value) {
|
|
8575
|
+
switch (value) {
|
|
8576
|
+
case "pull_request_created":
|
|
8577
|
+
return "paperclipPullRequestsCreatedCount";
|
|
8578
|
+
default:
|
|
8579
|
+
return void 0;
|
|
8580
|
+
}
|
|
8581
|
+
}
|
|
8582
|
+
async function persistCompanyActivityMetricEvent(ctx, params, options = {}) {
|
|
8583
|
+
const normalizedCompanyId = normalizeCompanyId(params.companyId);
|
|
8584
|
+
if (!normalizedCompanyId) {
|
|
8585
|
+
return {
|
|
8586
|
+
recorded: false
|
|
8587
|
+
};
|
|
8588
|
+
}
|
|
8589
|
+
const currentState = normalizeCompanyKpiState(await ctx.state.get(COMPANY_KPI_SCOPE));
|
|
8590
|
+
const result = recordCompanyActivityMetricEvent(currentState, {
|
|
8591
|
+
companyId: normalizedCompanyId,
|
|
8592
|
+
metric: params.metric,
|
|
8593
|
+
count: params.count,
|
|
8594
|
+
occurredAt: params.occurredAt,
|
|
8595
|
+
eventKey: params.eventKey,
|
|
8596
|
+
repositoryUrl: params.repositoryUrl,
|
|
8597
|
+
pullRequestNumber: params.pullRequestNumber,
|
|
8598
|
+
pullRequestUrl: params.pullRequestUrl
|
|
8599
|
+
});
|
|
8600
|
+
if (!result.recorded) {
|
|
8601
|
+
return {
|
|
8602
|
+
recorded: false,
|
|
8603
|
+
...result.eventKey ? { eventKey: result.eventKey } : {}
|
|
8604
|
+
};
|
|
8605
|
+
}
|
|
8606
|
+
try {
|
|
8607
|
+
await ctx.state.set(COMPANY_KPI_SCOPE, result.state);
|
|
8608
|
+
} catch (error) {
|
|
8609
|
+
if (options.throwOnPersistFailure) {
|
|
8610
|
+
throw error;
|
|
8611
|
+
}
|
|
8612
|
+
ctx.logger.warn("GitHub Sync could not persist a company activity metric event.", {
|
|
8613
|
+
companyId: normalizedCompanyId,
|
|
8614
|
+
metric: params.metric,
|
|
8615
|
+
repositoryUrl: params.repositoryUrl,
|
|
8616
|
+
pullRequestNumber: params.pullRequestNumber,
|
|
8617
|
+
error: getErrorMessage(error)
|
|
8618
|
+
});
|
|
8619
|
+
return {
|
|
8620
|
+
recorded: false,
|
|
8621
|
+
...result.eventKey ? { eventKey: result.eventKey } : {}
|
|
8622
|
+
};
|
|
8623
|
+
}
|
|
8624
|
+
return {
|
|
8625
|
+
recorded: true,
|
|
8626
|
+
...result.eventKey ? { eventKey: result.eventKey } : {}
|
|
8627
|
+
};
|
|
8628
|
+
}
|
|
8629
|
+
function parseWebhookPayloadRecord(input) {
|
|
8630
|
+
if (input.parsedBody && typeof input.parsedBody === "object" && !Array.isArray(input.parsedBody)) {
|
|
8631
|
+
return input.parsedBody;
|
|
8632
|
+
}
|
|
8633
|
+
const rawBody = input.rawBody.trim();
|
|
8634
|
+
if (!rawBody) {
|
|
8635
|
+
throw new Error("Webhook body must be a JSON object.");
|
|
8636
|
+
}
|
|
8637
|
+
let parsed;
|
|
8638
|
+
try {
|
|
8639
|
+
parsed = JSON.parse(rawBody);
|
|
8640
|
+
} catch {
|
|
8641
|
+
throw new Error("Webhook body must be valid JSON.");
|
|
8642
|
+
}
|
|
8643
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8644
|
+
throw new Error("Webhook body must be a JSON object.");
|
|
8645
|
+
}
|
|
8646
|
+
return parsed;
|
|
8647
|
+
}
|
|
8648
|
+
async function resolveCompanyIdForCompanyMetricEvent(ctx, params) {
|
|
8649
|
+
const requestedCompanyId = normalizeCompanyId(params.companyId);
|
|
8650
|
+
if (requestedCompanyId) {
|
|
8651
|
+
return requestedCompanyId;
|
|
8652
|
+
}
|
|
8653
|
+
if (!params.repositoryUrl) {
|
|
8654
|
+
return void 0;
|
|
8655
|
+
}
|
|
8656
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
8657
|
+
const matchingCompanyIds = [
|
|
8658
|
+
...new Set(
|
|
8659
|
+
settings.mappings.filter((mapping) => getNormalizedMappingRepositoryUrl(mapping) === params.repositoryUrl).map((mapping) => normalizeCompanyId(mapping.companyId)).filter((companyId) => Boolean(companyId))
|
|
8660
|
+
)
|
|
8661
|
+
];
|
|
8662
|
+
return matchingCompanyIds.length === 1 ? matchingCompanyIds[0] : void 0;
|
|
8663
|
+
}
|
|
8664
|
+
function getWebhookHeaderValue(headers, name) {
|
|
8665
|
+
const normalizedName = name.trim().toLowerCase();
|
|
8666
|
+
for (const [headerName, headerValue] of Object.entries(headers)) {
|
|
8667
|
+
if (headerName.trim().toLowerCase() !== normalizedName) {
|
|
8668
|
+
continue;
|
|
8669
|
+
}
|
|
8670
|
+
if (typeof headerValue === "string") {
|
|
8671
|
+
const trimmedValue = headerValue.trim();
|
|
8672
|
+
return trimmedValue || void 0;
|
|
8673
|
+
}
|
|
8674
|
+
if (!Array.isArray(headerValue)) {
|
|
8675
|
+
continue;
|
|
8676
|
+
}
|
|
8677
|
+
for (const entry of headerValue) {
|
|
8678
|
+
if (typeof entry !== "string") {
|
|
8679
|
+
continue;
|
|
8680
|
+
}
|
|
8681
|
+
const trimmedValue = entry.trim();
|
|
8682
|
+
if (trimmedValue) {
|
|
8683
|
+
return trimmedValue;
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
return void 0;
|
|
8688
|
+
}
|
|
8689
|
+
function normalizeCompanyMetricWebhookBearerToken(value) {
|
|
8690
|
+
if (!value) {
|
|
8691
|
+
return void 0;
|
|
8692
|
+
}
|
|
8693
|
+
const trimmedValue = value.trim();
|
|
8694
|
+
if (!trimmedValue) {
|
|
8695
|
+
return void 0;
|
|
8696
|
+
}
|
|
8697
|
+
const bearerMatch = trimmedValue.match(/^Bearer\s+(.+)$/i);
|
|
8698
|
+
if (!bearerMatch) {
|
|
8699
|
+
return void 0;
|
|
8700
|
+
}
|
|
8701
|
+
const token = bearerMatch[1]?.trim();
|
|
8702
|
+
return token || void 0;
|
|
8703
|
+
}
|
|
8704
|
+
function normalizePaperclipCurrentAgentRecord(value) {
|
|
8705
|
+
if (!value || typeof value !== "object") {
|
|
8706
|
+
return null;
|
|
8707
|
+
}
|
|
8708
|
+
const record = value;
|
|
8709
|
+
const id = normalizeOptionalString2(record.id);
|
|
8710
|
+
const companyId = normalizeCompanyId(record.companyId);
|
|
8711
|
+
return id && companyId ? {
|
|
8712
|
+
id,
|
|
8713
|
+
companyId
|
|
8714
|
+
} : null;
|
|
8715
|
+
}
|
|
8716
|
+
async function readCompanyMetricWebhookCurrentAgent(paperclipApiBaseUrl, bearerToken) {
|
|
8717
|
+
const response = await fetchPaperclipApi(getPaperclipCurrentAgentEndpoint(paperclipApiBaseUrl), {
|
|
8718
|
+
method: "GET",
|
|
8719
|
+
headers: {
|
|
8720
|
+
accept: "application/json",
|
|
8721
|
+
authorization: `Bearer ${bearerToken}`
|
|
8722
|
+
}
|
|
8723
|
+
});
|
|
8724
|
+
const payloadResult = await readPaperclipApiJsonResponse(response, {
|
|
8725
|
+
operationLabel: "current agent"
|
|
8726
|
+
});
|
|
8727
|
+
if (payloadResult.failure) {
|
|
8728
|
+
if (payloadResult.failure.requiresAuthentication) {
|
|
8729
|
+
throw new Error("Company KPI webhook Authorization must be a valid PAPERCLIP_API_KEY bearer token.");
|
|
8730
|
+
}
|
|
8731
|
+
const detail = payloadResult.failure.errorMessage ? ` ${payloadResult.failure.errorMessage}` : "";
|
|
8732
|
+
throw new Error(`Could not validate the KPI webhook Paperclip API key.${detail}`);
|
|
8733
|
+
}
|
|
8734
|
+
const agent = normalizePaperclipCurrentAgentRecord(payloadResult.data);
|
|
8735
|
+
if (!agent) {
|
|
8736
|
+
throw new Error("Paperclip did not return a usable current agent record while validating the KPI webhook caller.");
|
|
8737
|
+
}
|
|
8738
|
+
return agent;
|
|
8739
|
+
}
|
|
8740
|
+
async function assertCompanyMetricWebhookAuthenticated(ctx, input, companyId) {
|
|
8741
|
+
const rawAuthorization = getWebhookHeaderValue(input.headers, COMPANY_METRIC_WEBHOOK_AUTH_HEADER);
|
|
8742
|
+
const bearerToken = normalizeCompanyMetricWebhookBearerToken(rawAuthorization);
|
|
8743
|
+
if (!bearerToken) {
|
|
8744
|
+
throw new Error(
|
|
8745
|
+
`Missing or invalid ${COMPANY_METRIC_WEBHOOK_AUTH_HEADER} header. Use Bearer <PAPERCLIP_API_KEY>.`
|
|
8746
|
+
);
|
|
8747
|
+
}
|
|
8748
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
8749
|
+
const config = await getResolvedConfig(ctx);
|
|
8750
|
+
const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(settings, config, companyId);
|
|
8751
|
+
if (!paperclipApiBaseUrl) {
|
|
8752
|
+
throw new Error(
|
|
8753
|
+
"A trusted Paperclip API origin is required to validate PAPERCLIP_API_KEY. Set PAPERCLIP_API_URL or save the Paperclip host origin before sending KPI webhook events."
|
|
8754
|
+
);
|
|
8755
|
+
}
|
|
8756
|
+
const currentAgent = await readCompanyMetricWebhookCurrentAgent(paperclipApiBaseUrl, bearerToken);
|
|
8757
|
+
if (normalizeCompanyId(currentAgent.companyId) !== companyId) {
|
|
8758
|
+
throw new Error("Company KPI webhook Paperclip API key belongs to a different company.");
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
async function handleCompanyMetricWebhook(ctx, input) {
|
|
8762
|
+
if (input.endpointKey !== COMPANY_METRIC_WEBHOOK_ENDPOINT_KEY) {
|
|
8763
|
+
throw new Error(`Unsupported webhook endpoint: ${input.endpointKey}.`);
|
|
8764
|
+
}
|
|
8765
|
+
const payload = parseWebhookPayloadRecord(input);
|
|
8766
|
+
const repositoryInput = normalizeOptionalString2(payload.repository);
|
|
8767
|
+
const repository = repositoryInput ? parseRepositoryReference(repositoryInput) : null;
|
|
8768
|
+
if (repositoryInput && !repository) {
|
|
8769
|
+
throw new Error("repository must be owner/repo or https://github.com/owner/repo.");
|
|
8770
|
+
}
|
|
8771
|
+
const companyId = await resolveCompanyIdForCompanyMetricEvent(ctx, {
|
|
8772
|
+
companyId: payload.companyId,
|
|
8773
|
+
repositoryUrl: repository?.url
|
|
8774
|
+
});
|
|
8775
|
+
if (!companyId) {
|
|
8776
|
+
throw new Error("companyId is required unless repository maps to exactly one company.");
|
|
8777
|
+
}
|
|
8778
|
+
await assertCompanyMetricWebhookAuthenticated(ctx, input, companyId);
|
|
8779
|
+
const metric = normalizeCompanyActivityMetricInputValue(payload.metric);
|
|
8780
|
+
if (!metric) {
|
|
8781
|
+
throw new Error('metric must be "pull_request_created".');
|
|
8782
|
+
}
|
|
8783
|
+
const pullRequestNumber = normalizeToolPositiveInteger(payload.pullRequestNumber);
|
|
8784
|
+
const pullRequestUrl = normalizeGitHubPullRequestHtmlUrl(normalizeOptionalString2(payload.pullRequestUrl));
|
|
8785
|
+
const eventKey = normalizeOptionalString2(payload.eventKey);
|
|
8786
|
+
const dedupeKey = buildCompanyMetricEventKey({
|
|
8787
|
+
metric,
|
|
8788
|
+
eventKey,
|
|
8789
|
+
repositoryUrl: repository?.url,
|
|
8790
|
+
pullRequestNumber,
|
|
8791
|
+
pullRequestUrl
|
|
8792
|
+
});
|
|
8793
|
+
if (!dedupeKey) {
|
|
8794
|
+
throw new Error(
|
|
8795
|
+
"Company KPI webhook requires pullRequestUrl, repository plus pullRequestNumber, or eventKey so duplicate deliveries can be ignored."
|
|
8796
|
+
);
|
|
8797
|
+
}
|
|
8798
|
+
const recordedMetric = await persistCompanyActivityMetricEvent(
|
|
8799
|
+
ctx,
|
|
8800
|
+
{
|
|
8801
|
+
companyId,
|
|
8802
|
+
metric,
|
|
8803
|
+
count: normalizeToolPositiveInteger(payload.count),
|
|
8804
|
+
occurredAt: normalizeOptionalString2(payload.occurredAt),
|
|
8805
|
+
eventKey,
|
|
8806
|
+
repositoryUrl: repository?.url,
|
|
8807
|
+
pullRequestNumber,
|
|
8808
|
+
pullRequestUrl
|
|
8809
|
+
},
|
|
8810
|
+
{
|
|
8811
|
+
throwOnPersistFailure: true
|
|
8812
|
+
}
|
|
8813
|
+
);
|
|
8814
|
+
ctx.logger.info(
|
|
8815
|
+
recordedMetric.recorded ? "GitHub Sync recorded a company KPI webhook event." : "GitHub Sync ignored a duplicate company KPI webhook event.",
|
|
8816
|
+
{
|
|
8817
|
+
endpointKey: input.endpointKey,
|
|
8818
|
+
companyId,
|
|
8819
|
+
metric,
|
|
8820
|
+
repositoryUrl: repository?.url,
|
|
8821
|
+
pullRequestNumber,
|
|
8822
|
+
pullRequestUrl,
|
|
8823
|
+
requestId: input.requestId
|
|
8824
|
+
}
|
|
8825
|
+
);
|
|
8826
|
+
}
|
|
7830
8827
|
async function createGitHubToolOctokit(ctx, companyId) {
|
|
7831
8828
|
const token = (await resolveGithubToken(ctx, { companyId })).trim();
|
|
7832
8829
|
if (!token) {
|
|
@@ -10782,6 +11779,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
10782
11779
|
const config = await getResolvedConfig(ctx);
|
|
10783
11780
|
const settings = materializeScopedSettings(baseSettings, config, targetCompanyId);
|
|
10784
11781
|
const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
|
|
11782
|
+
let companyKpiState = normalizeCompanyKpiState(await ctx.state.get(COMPANY_KPI_SCOPE));
|
|
11783
|
+
let companyKpiStateDirty = false;
|
|
10785
11784
|
const token = typeof options.resolvedToken === "string" ? options.resolvedToken : await resolveGithubToken(ctx, { companyId: targetCompanyId });
|
|
10786
11785
|
const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(baseSettings, config, targetCompanyId);
|
|
10787
11786
|
const mappings = getSyncableMappingsForTarget(settings.mappings, options.target);
|
|
@@ -10866,6 +11865,19 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
10866
11865
|
erroredIssuesCount: recoverableFailures.length,
|
|
10867
11866
|
progress: currentProgress
|
|
10868
11867
|
});
|
|
11868
|
+
async function flushCompanyKpiState() {
|
|
11869
|
+
if (!companyKpiStateDirty) {
|
|
11870
|
+
return;
|
|
11871
|
+
}
|
|
11872
|
+
try {
|
|
11873
|
+
await ctx.state.set(COMPANY_KPI_SCOPE, companyKpiState);
|
|
11874
|
+
companyKpiStateDirty = false;
|
|
11875
|
+
} catch (error) {
|
|
11876
|
+
ctx.logger.warn("GitHub Sync could not persist company KPI state.", {
|
|
11877
|
+
error: getErrorMessage(error)
|
|
11878
|
+
});
|
|
11879
|
+
}
|
|
11880
|
+
}
|
|
10869
11881
|
async function throwIfSyncCancelled() {
|
|
10870
11882
|
const cancellationRequest = await getSyncCancellationRequest(ctx);
|
|
10871
11883
|
if (!cancellationRequest) {
|
|
@@ -10918,6 +11930,56 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
10918
11930
|
completedTrackedIssueKeys.add(key);
|
|
10919
11931
|
completedTrackedIssueCount += 1;
|
|
10920
11932
|
}
|
|
11933
|
+
function markTrackedPullRequestIssueProcessed(mapping, record) {
|
|
11934
|
+
const key = buildTrackedPullRequestIssueProgressKey(mapping, record);
|
|
11935
|
+
if (completedTrackedIssueKeys.has(key)) {
|
|
11936
|
+
return;
|
|
11937
|
+
}
|
|
11938
|
+
completedTrackedIssueKeys.add(key);
|
|
11939
|
+
completedTrackedIssueCount += 1;
|
|
11940
|
+
}
|
|
11941
|
+
function recordCompanyBacklogSnapshotsFromPlans(repositoryPlans2) {
|
|
11942
|
+
if (options.target?.kind === "project" || options.target?.kind === "issue") {
|
|
11943
|
+
return;
|
|
11944
|
+
}
|
|
11945
|
+
const expectedMappingsByCompanyId = /* @__PURE__ */ new Map();
|
|
11946
|
+
for (const mapping of mappings) {
|
|
11947
|
+
const companyId = normalizeCompanyId(mapping.companyId);
|
|
11948
|
+
if (!companyId) {
|
|
11949
|
+
continue;
|
|
11950
|
+
}
|
|
11951
|
+
expectedMappingsByCompanyId.set(companyId, (expectedMappingsByCompanyId.get(companyId) ?? 0) + 1);
|
|
11952
|
+
}
|
|
11953
|
+
const planBacklogByCompanyId = /* @__PURE__ */ new Map();
|
|
11954
|
+
for (const plan of repositoryPlans2) {
|
|
11955
|
+
const companyId = normalizeCompanyId(plan.mapping.companyId);
|
|
11956
|
+
if (!companyId) {
|
|
11957
|
+
continue;
|
|
11958
|
+
}
|
|
11959
|
+
const current = planBacklogByCompanyId.get(companyId) ?? {
|
|
11960
|
+
repositoryCount: 0,
|
|
11961
|
+
openIssueCount: 0
|
|
11962
|
+
};
|
|
11963
|
+
current.repositoryCount += 1;
|
|
11964
|
+
current.openIssueCount += plan.issues.length;
|
|
11965
|
+
planBacklogByCompanyId.set(companyId, current);
|
|
11966
|
+
}
|
|
11967
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11968
|
+
const day = getIsoDayString(capturedAt);
|
|
11969
|
+
for (const [companyId, expectedRepositoryCount] of expectedMappingsByCompanyId.entries()) {
|
|
11970
|
+
const planned = planBacklogByCompanyId.get(companyId);
|
|
11971
|
+
if (!planned || planned.repositoryCount !== expectedRepositoryCount) {
|
|
11972
|
+
continue;
|
|
11973
|
+
}
|
|
11974
|
+
companyKpiState = upsertCompanyBacklogSnapshot(companyKpiState, companyId, {
|
|
11975
|
+
day,
|
|
11976
|
+
capturedAt,
|
|
11977
|
+
openIssueCount: planned.openIssueCount,
|
|
11978
|
+
repositoryCount: planned.repositoryCount
|
|
11979
|
+
});
|
|
11980
|
+
companyKpiStateDirty = true;
|
|
11981
|
+
}
|
|
11982
|
+
}
|
|
10921
11983
|
const repositoryPlans = [];
|
|
10922
11984
|
try {
|
|
10923
11985
|
await throwIfSyncCancelled();
|
|
@@ -10983,12 +12045,13 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
10983
12045
|
const importRegistryByIssueId = new Map(
|
|
10984
12046
|
importedIssueRecords.map((entry) => [entry.githubIssueId, entry])
|
|
10985
12047
|
);
|
|
12048
|
+
const pullRequestLinks = await listGitHubPullRequestIssueLinksForMapping(ctx, mapping, options.target);
|
|
10986
12049
|
const ensuredPaperclipIssueIds = /* @__PURE__ */ new Map();
|
|
10987
12050
|
const trackedIssueIds = /* @__PURE__ */ new Set([
|
|
10988
12051
|
...issues.map((issue) => issue.id),
|
|
10989
12052
|
...importRegistryByIssueId.keys()
|
|
10990
12053
|
]);
|
|
10991
|
-
const trackedIssueCount = [...trackedIssueIds].filter((issueId) => allIssuesById.has(issueId)).length;
|
|
12054
|
+
const trackedIssueCount = [...trackedIssueIds].filter((issueId) => allIssuesById.has(issueId)).length + pullRequestLinks.length;
|
|
10992
12055
|
totalTrackedIssueCount += trackedIssueCount;
|
|
10993
12056
|
syncedIssuesCount = totalTrackedIssueCount;
|
|
10994
12057
|
currentProgress = {
|
|
@@ -11007,6 +12070,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11007
12070
|
allIssues: eligibleIssues,
|
|
11008
12071
|
issues,
|
|
11009
12072
|
allIssuesById,
|
|
12073
|
+
pullRequestLinks,
|
|
11010
12074
|
trackedIssueCount
|
|
11011
12075
|
});
|
|
11012
12076
|
} catch (error) {
|
|
@@ -11017,6 +12081,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11017
12081
|
continue;
|
|
11018
12082
|
}
|
|
11019
12083
|
}
|
|
12084
|
+
recordCompanyBacklogSnapshotsFromPlans(repositoryPlans);
|
|
11020
12085
|
if (repositoryPlans.length > 0) {
|
|
11021
12086
|
const firstPlan = repositoryPlans[0];
|
|
11022
12087
|
currentProgress = {
|
|
@@ -11041,7 +12106,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11041
12106
|
for (const plan of repositoryPlans) {
|
|
11042
12107
|
await throwIfSyncCancelled();
|
|
11043
12108
|
try {
|
|
11044
|
-
const { mapping, advancedSettings, repository, repositoryIndex, allIssuesById, issues } = plan;
|
|
12109
|
+
const { mapping, advancedSettings, repository, repositoryIndex, allIssuesById, issues, pullRequestLinks } = plan;
|
|
11045
12110
|
const companyId = mapping.companyId;
|
|
11046
12111
|
let availableLabels = companyId ? companyLabelDirectoryCache.get(companyId) : void 0;
|
|
11047
12112
|
if (!availableLabels) {
|
|
@@ -11094,6 +12159,15 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11094
12159
|
}
|
|
11095
12160
|
}
|
|
11096
12161
|
}
|
|
12162
|
+
for (const pullRequestLink of pullRequestLinks) {
|
|
12163
|
+
const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
|
|
12164
|
+
const entry = openLinkedPullRequestNumbersByRepository.get(pullRequestRepository.url) ?? {
|
|
12165
|
+
repository: pullRequestRepository,
|
|
12166
|
+
numbers: /* @__PURE__ */ new Set()
|
|
12167
|
+
};
|
|
12168
|
+
entry.numbers.add(pullRequestLink.data.githubPullRequestNumber);
|
|
12169
|
+
openLinkedPullRequestNumbersByRepository.set(pullRequestRepository.url, entry);
|
|
12170
|
+
}
|
|
11097
12171
|
for (const entry of openLinkedPullRequestNumbersByRepository.values()) {
|
|
11098
12172
|
await warmGitHubPullRequestStatusCache(
|
|
11099
12173
|
octokit,
|
|
@@ -11203,6 +12277,17 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11203
12277
|
failureContext,
|
|
11204
12278
|
recoverableFailures,
|
|
11205
12279
|
throwIfSyncCancelled,
|
|
12280
|
+
async (params) => {
|
|
12281
|
+
const recorded = recordCompanyActivityMetricEvent(companyKpiState, {
|
|
12282
|
+
companyId: params.companyId,
|
|
12283
|
+
metric: "githubIssuesClosedCount",
|
|
12284
|
+
eventKey: `${params.repositoryUrl}/issues/${params.githubIssueNumber}:${coerceDate(params.occurredAt).toISOString()}`,
|
|
12285
|
+
repositoryUrl: params.repositoryUrl,
|
|
12286
|
+
occurredAt: params.occurredAt
|
|
12287
|
+
});
|
|
12288
|
+
companyKpiState = recorded.state;
|
|
12289
|
+
companyKpiStateDirty = companyKpiStateDirty || recorded.recorded;
|
|
12290
|
+
},
|
|
11206
12291
|
async (progress) => {
|
|
11207
12292
|
markTrackedIssueProcessed(mapping, progress.githubIssueId);
|
|
11208
12293
|
currentProgress = {
|
|
@@ -11220,6 +12305,33 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11220
12305
|
updatedStatusesCount += synchronizationResult.updatedStatusesCount;
|
|
11221
12306
|
updatedLabelsCount += synchronizationResult.updatedLabelsCount;
|
|
11222
12307
|
updatedDescriptionsCount += synchronizationResult.updatedDescriptionsCount;
|
|
12308
|
+
const pullRequestSynchronizationResult = await synchronizePaperclipPullRequestIssueStatuses(
|
|
12309
|
+
ctx,
|
|
12310
|
+
octokit,
|
|
12311
|
+
mapping,
|
|
12312
|
+
advancedSettings,
|
|
12313
|
+
pullRequestLinks,
|
|
12314
|
+
paperclipApiBaseUrl,
|
|
12315
|
+
pullRequestStatusCache,
|
|
12316
|
+
failureContext,
|
|
12317
|
+
recoverableFailures,
|
|
12318
|
+
throwIfSyncCancelled,
|
|
12319
|
+
async (progress) => {
|
|
12320
|
+
markTrackedPullRequestIssueProcessed(mapping, progress.pullRequestLink);
|
|
12321
|
+
const pullRequestRepository = requireRepositoryReference(progress.pullRequestLink.data.repositoryUrl);
|
|
12322
|
+
currentProgress = {
|
|
12323
|
+
phase: "syncing",
|
|
12324
|
+
totalRepositoryCount: mappings.length,
|
|
12325
|
+
currentRepositoryIndex: repositoryIndex,
|
|
12326
|
+
currentRepositoryUrl: repository.url,
|
|
12327
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
12328
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
12329
|
+
detailLabel: `Synced pull request #${progress.pullRequestLink.data.githubPullRequestNumber} in ${pullRequestRepository.owner}/${pullRequestRepository.repo}.`
|
|
12330
|
+
};
|
|
12331
|
+
await persistRunningProgress(progress.completedIssueCount === progress.totalIssueCount);
|
|
12332
|
+
}
|
|
12333
|
+
);
|
|
12334
|
+
updatedStatusesCount += pullRequestSynchronizationResult.updatedStatusesCount;
|
|
11223
12335
|
} catch (error) {
|
|
11224
12336
|
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
11225
12337
|
throw error;
|
|
@@ -11306,6 +12418,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
11306
12418
|
await saveSettingsSyncState(ctx, currentSettings, next.syncState, targetCompanyId);
|
|
11307
12419
|
await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
|
|
11308
12420
|
return materializeScopedSettings(next, config, targetCompanyId);
|
|
12421
|
+
} finally {
|
|
12422
|
+
await flushCompanyKpiState();
|
|
11309
12423
|
}
|
|
11310
12424
|
}
|
|
11311
12425
|
async function startSync(ctx, trigger, options = {}) {
|
|
@@ -11687,6 +12801,19 @@ function registerGitHubAgentTools(ctx) {
|
|
|
11687
12801
|
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
11688
12802
|
}
|
|
11689
12803
|
});
|
|
12804
|
+
await persistCompanyActivityMetricEvent(
|
|
12805
|
+
ctx,
|
|
12806
|
+
{
|
|
12807
|
+
companyId: runCtx.companyId,
|
|
12808
|
+
metric: "paperclipPullRequestsCreatedCount",
|
|
12809
|
+
repositoryUrl: repository.url,
|
|
12810
|
+
pullRequestNumber: response.data.number,
|
|
12811
|
+
pullRequestUrl: response.data.html_url ?? void 0
|
|
12812
|
+
},
|
|
12813
|
+
{
|
|
12814
|
+
throwOnPersistFailure: false
|
|
12815
|
+
}
|
|
12816
|
+
);
|
|
11690
12817
|
return buildToolSuccessResult(
|
|
11691
12818
|
`Created pull request #${response.data.number} in ${formatRepositoryLabel(repository)}.`,
|
|
11692
12819
|
{
|
|
@@ -12184,6 +13311,7 @@ function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
|
12184
13311
|
}
|
|
12185
13312
|
var plugin = definePlugin({
|
|
12186
13313
|
async setup(ctx) {
|
|
13314
|
+
pluginRuntimeContext = ctx;
|
|
12187
13315
|
ctx.data.register("settings.registration", async (input) => {
|
|
12188
13316
|
const record = input && typeof input === "object" ? input : {};
|
|
12189
13317
|
const requestedCompanyId = normalizeCompanyId(record.companyId);
|
|
@@ -12218,6 +13346,10 @@ var plugin = definePlugin({
|
|
|
12218
13346
|
paperclipBoardAccessNeedsConfigSync: Boolean(savedBoardTokenRef && !configuredBoardTokenRef)
|
|
12219
13347
|
};
|
|
12220
13348
|
});
|
|
13349
|
+
ctx.data.register("dashboard.metrics", async (input) => {
|
|
13350
|
+
const record = input && typeof input === "object" ? input : {};
|
|
13351
|
+
return buildDashboardMetricsData(ctx, record);
|
|
13352
|
+
});
|
|
12221
13353
|
ctx.data.register("sync.toolbarState", async (input) => {
|
|
12222
13354
|
const record = input && typeof input === "object" ? input : {};
|
|
12223
13355
|
return buildToolbarSyncState(ctx, record);
|
|
@@ -12543,6 +13675,15 @@ var plugin = definePlugin({
|
|
|
12543
13675
|
await startSync(ctx, trigger, target ? { target } : {});
|
|
12544
13676
|
}
|
|
12545
13677
|
});
|
|
13678
|
+
},
|
|
13679
|
+
async onWebhook(input) {
|
|
13680
|
+
if (!pluginRuntimeContext) {
|
|
13681
|
+
throw new Error("GitHub Sync worker is not ready to handle webhooks yet.");
|
|
13682
|
+
}
|
|
13683
|
+
await handleCompanyMetricWebhook(pluginRuntimeContext, input);
|
|
13684
|
+
},
|
|
13685
|
+
async onShutdown() {
|
|
13686
|
+
pluginRuntimeContext = null;
|
|
12546
13687
|
}
|
|
12547
13688
|
});
|
|
12548
13689
|
var worker_default = plugin;
|