@triagepilot/db 1.1.0
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/LICENSE +105 -0
- package/dist/availability.d.ts +164 -0
- package/dist/availability.js +974 -0
- package/dist/database.d.ts +3 -0
- package/dist/database.js +9 -0
- package/dist/decisions.d.ts +107 -0
- package/dist/decisions.js +435 -0
- package/dist/deliveries.d.ts +30 -0
- package/dist/deliveries.js +82 -0
- package/dist/heartbeat.d.ts +11 -0
- package/dist/heartbeat.js +11 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/jobs.d.ts +86 -0
- package/dist/jobs.js +641 -0
- package/dist/kysely.d.ts +202 -0
- package/dist/kysely.js +1 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +45 -0
- package/dist/operations.d.ts +77 -0
- package/dist/operations.js +256 -0
- package/dist/outbox.d.ts +61 -0
- package/dist/outbox.js +230 -0
- package/dist/provider-connections.d.ts +34 -0
- package/dist/provider-connections.js +302 -0
- package/dist/retention.d.ts +6 -0
- package/dist/retention.js +44 -0
- package/dist/routing-recovery.d.ts +55 -0
- package/dist/routing-recovery.js +175 -0
- package/dist/workspaces.d.ts +50 -0
- package/dist/workspaces.js +43 -0
- package/migrations/0001_initial.sql +79 -0
- package/migrations/0002_selected_reviewers.sql +10 -0
- package/migrations/0003_human_review_policy.sql +10 -0
- package/migrations/0004_semantic_routing_deduplication.sql +15 -0
- package/migrations/0005_reviewer_availability.sql +58 -0
- package/migrations/0005_workspace_scope.sql +198 -0
- package/migrations/0006_decision_outbox.sql +24 -0
- package/migrations/0007_workspace_reviewer_availability.sql +192 -0
- package/migrations/0008_reviewer_mutation_intents.sql +160 -0
- package/migrations/0009_provider_connection_revocations.sql +18 -0
- package/migrations/0010_provider_connection_preemptive_revocations.sql +12 -0
- package/package.json +42 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
import { lockProviderConnectionProjection } from "./provider-connections.js";
|
|
3
|
+
export function createWorkspaceRoutingRecoveryRepository(db, workspaceId) {
|
|
4
|
+
return {
|
|
5
|
+
findTarget: (request) => findRoutingRecoveryTarget(db, workspaceId, request),
|
|
6
|
+
findActiveRepository: (input) => findActiveRecoveryRepository(db, workspaceId, input),
|
|
7
|
+
findActiveExternalConnectionId: (input) => findActiveExternalConnectionId(db, workspaceId, input),
|
|
8
|
+
enqueue: (input) => enqueueRoutingRecovery(db, workspaceId, input),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export async function findRoutingRecoveryTarget(db, workspaceId, request) {
|
|
12
|
+
if ("decisionId" in request) {
|
|
13
|
+
const target = await db
|
|
14
|
+
.selectFrom("routing_decisions")
|
|
15
|
+
.innerJoin("repositories", (join) => join
|
|
16
|
+
.onRef("repositories.id", "=", "routing_decisions.repository_id")
|
|
17
|
+
.onRef("repositories.workspace_id", "=", "routing_decisions.workspace_id"))
|
|
18
|
+
.innerJoin("provider_connections", (join) => join
|
|
19
|
+
.onRef("provider_connections.id", "=", "repositories.provider_connection_id")
|
|
20
|
+
.onRef("provider_connections.workspace_id", "=", "repositories.workspace_id")
|
|
21
|
+
.onRef("provider_connections.provider", "=", "repositories.provider"))
|
|
22
|
+
.select([
|
|
23
|
+
"repositories.provider",
|
|
24
|
+
"repositories.provider_connection_id",
|
|
25
|
+
"repositories.external_repository_id",
|
|
26
|
+
"repositories.owner",
|
|
27
|
+
"repositories.name",
|
|
28
|
+
"routing_decisions.change_request_id",
|
|
29
|
+
"routing_decisions.pull_number",
|
|
30
|
+
])
|
|
31
|
+
.where("routing_decisions.workspace_id", "=", workspaceId)
|
|
32
|
+
.where("routing_decisions.id", "=", request.decisionId)
|
|
33
|
+
.where("provider_connections.status", "=", "active")
|
|
34
|
+
.executeTakeFirst();
|
|
35
|
+
return target === undefined ? null : toRoutingRecoveryTarget(target);
|
|
36
|
+
}
|
|
37
|
+
const reference = request.changeRequest;
|
|
38
|
+
const target = await db
|
|
39
|
+
.selectFrom("repositories")
|
|
40
|
+
.innerJoin("provider_connections", (join) => join
|
|
41
|
+
.onRef("provider_connections.id", "=", "repositories.provider_connection_id")
|
|
42
|
+
.onRef("provider_connections.workspace_id", "=", "repositories.workspace_id")
|
|
43
|
+
.onRef("provider_connections.provider", "=", "repositories.provider"))
|
|
44
|
+
.select([
|
|
45
|
+
"repositories.provider",
|
|
46
|
+
"repositories.provider_connection_id",
|
|
47
|
+
"repositories.external_repository_id",
|
|
48
|
+
"repositories.owner",
|
|
49
|
+
"repositories.name",
|
|
50
|
+
])
|
|
51
|
+
.where("repositories.workspace_id", "=", workspaceId)
|
|
52
|
+
.where("repositories.provider", "=", reference.repository.provider)
|
|
53
|
+
.where("repositories.external_repository_id", "=", reference.repository.externalId)
|
|
54
|
+
.where("repositories.owner", "=", reference.repository.owner)
|
|
55
|
+
.where("repositories.name", "=", reference.repository.name)
|
|
56
|
+
.where("provider_connections.status", "=", "active")
|
|
57
|
+
.executeTakeFirst();
|
|
58
|
+
if (target === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
return toRoutingRecoveryTarget({
|
|
61
|
+
...target,
|
|
62
|
+
change_request_id: reference.externalId,
|
|
63
|
+
pull_number: reference.number,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export async function findActiveRecoveryRepository(db, workspaceId, input) {
|
|
67
|
+
const matches = await db
|
|
68
|
+
.selectFrom("repositories")
|
|
69
|
+
.innerJoin("provider_connections", (join) => join
|
|
70
|
+
.onRef("provider_connections.id", "=", "repositories.provider_connection_id")
|
|
71
|
+
.onRef("provider_connections.workspace_id", "=", "repositories.workspace_id")
|
|
72
|
+
.onRef("provider_connections.provider", "=", "repositories.provider"))
|
|
73
|
+
.select([
|
|
74
|
+
"repositories.provider",
|
|
75
|
+
"repositories.external_repository_id",
|
|
76
|
+
"repositories.owner",
|
|
77
|
+
"repositories.name",
|
|
78
|
+
])
|
|
79
|
+
.where("repositories.workspace_id", "=", workspaceId)
|
|
80
|
+
.where("repositories.provider", "=", input.provider)
|
|
81
|
+
.where(sql `lower(repositories.owner) = lower(${input.owner})`)
|
|
82
|
+
.where(sql `lower(repositories.name) = lower(${input.name})`)
|
|
83
|
+
.where("provider_connections.status", "=", "active")
|
|
84
|
+
.limit(2)
|
|
85
|
+
.execute();
|
|
86
|
+
if (matches.length !== 1)
|
|
87
|
+
return null;
|
|
88
|
+
const repository = matches[0];
|
|
89
|
+
return {
|
|
90
|
+
provider: repository.provider,
|
|
91
|
+
externalId: repository.external_repository_id,
|
|
92
|
+
owner: repository.owner,
|
|
93
|
+
name: repository.name,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function findActiveExternalConnectionId(db, workspaceId, input) {
|
|
97
|
+
const connection = await db.selectFrom("provider_connections")
|
|
98
|
+
.select("external_connection_id")
|
|
99
|
+
.where("workspace_id", "=", workspaceId)
|
|
100
|
+
.where("provider", "=", input.provider)
|
|
101
|
+
.where("id", "=", input.providerConnectionId)
|
|
102
|
+
.where("status", "=", "active")
|
|
103
|
+
.executeTakeFirst();
|
|
104
|
+
return connection?.external_connection_id ?? null;
|
|
105
|
+
}
|
|
106
|
+
export async function enqueueRoutingRecovery(db, workspaceId, input) {
|
|
107
|
+
if (!matchesEnqueueScope(workspaceId, input))
|
|
108
|
+
return null;
|
|
109
|
+
return await db.transaction().execute(async (trx) => {
|
|
110
|
+
await lockProviderConnectionProjection(trx, workspaceId);
|
|
111
|
+
const repository = await trx
|
|
112
|
+
.selectFrom("repositories")
|
|
113
|
+
.innerJoin("provider_connections", (join) => join
|
|
114
|
+
.onRef("provider_connections.id", "=", "repositories.provider_connection_id")
|
|
115
|
+
.onRef("provider_connections.workspace_id", "=", "repositories.workspace_id")
|
|
116
|
+
.onRef("provider_connections.provider", "=", "repositories.provider"))
|
|
117
|
+
.select("repositories.id")
|
|
118
|
+
.where("repositories.workspace_id", "=", workspaceId)
|
|
119
|
+
.where("repositories.provider", "=", input.provider)
|
|
120
|
+
.where("repositories.provider_connection_id", "=", input.providerConnectionId)
|
|
121
|
+
.where("repositories.external_repository_id", "=", input.payload.changeRequest.repository.externalId)
|
|
122
|
+
.where("repositories.owner", "=", input.payload.changeRequest.repository.owner)
|
|
123
|
+
.where("repositories.name", "=", input.payload.changeRequest.repository.name)
|
|
124
|
+
.where("provider_connections.status", "=", "active")
|
|
125
|
+
.executeTakeFirst();
|
|
126
|
+
if (repository === undefined)
|
|
127
|
+
return null;
|
|
128
|
+
const inserted = await trx.insertInto("jobs").values({
|
|
129
|
+
workspace_id: workspaceId,
|
|
130
|
+
provider: input.provider,
|
|
131
|
+
provider_connection_id: input.providerConnectionId,
|
|
132
|
+
kind: "process_pull_request",
|
|
133
|
+
payload: input.payload,
|
|
134
|
+
idempotency_key: input.idempotencyKey,
|
|
135
|
+
}).onConflict((conflict) => conflict
|
|
136
|
+
.columns(["workspace_id", "idempotency_key"])
|
|
137
|
+
.doNothing())
|
|
138
|
+
.returning("id")
|
|
139
|
+
.executeTakeFirst();
|
|
140
|
+
if (inserted !== undefined)
|
|
141
|
+
return { inserted: true, jobId: inserted.id };
|
|
142
|
+
const existing = await trx.selectFrom("jobs")
|
|
143
|
+
.select("id")
|
|
144
|
+
.where("workspace_id", "=", workspaceId)
|
|
145
|
+
.where("provider", "=", input.provider)
|
|
146
|
+
.where("provider_connection_id", "=", input.providerConnectionId)
|
|
147
|
+
.where("kind", "=", "process_pull_request")
|
|
148
|
+
.where("idempotency_key", "=", input.idempotencyKey)
|
|
149
|
+
.executeTakeFirst();
|
|
150
|
+
return existing === undefined ? null : { inserted: false, jobId: existing.id };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function matchesEnqueueScope(workspaceId, input) {
|
|
154
|
+
return input.payload.workspaceId === workspaceId
|
|
155
|
+
&& input.payload.providerConnectionId === input.providerConnectionId
|
|
156
|
+
&& input.payload.changeRequest.repository.provider === input.provider
|
|
157
|
+
&& input.payload.routingKey === input.idempotencyKey;
|
|
158
|
+
}
|
|
159
|
+
function toRoutingRecoveryTarget(input) {
|
|
160
|
+
if (input.change_request_id === null || input.change_request_id.trim().length === 0
|
|
161
|
+
|| input.pull_number === null || !Number.isSafeInteger(input.pull_number) || input.pull_number < 1) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
providerConnectionId: input.provider_connection_id,
|
|
166
|
+
repository: {
|
|
167
|
+
provider: input.provider,
|
|
168
|
+
externalId: input.external_repository_id,
|
|
169
|
+
owner: input.owner,
|
|
170
|
+
name: input.name,
|
|
171
|
+
},
|
|
172
|
+
changeRequestId: input.change_request_id,
|
|
173
|
+
changeRequestNumber: input.pull_number,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import type { WorkspaceId } from "@triagepilot/contracts";
|
|
3
|
+
import type { Database } from "./kysely.js";
|
|
4
|
+
import { type DecisionInput, type HumanReviewPolicyDecision, type PersistedDecision } from "./decisions.js";
|
|
5
|
+
import { type HumanReviewPolicyDeliveryInput, type RoutingDeliveryInput } from "./deliveries.js";
|
|
6
|
+
import { type WorkspaceJobQueue } from "./jobs.js";
|
|
7
|
+
import { type OperationsOverview, type ReadOperationsOverviewInput, type RepositoryConfigurationTarget } from "./operations.js";
|
|
8
|
+
import { type ConfiguredProviderConnectionInput, type ProviderConnectionMetadata, type ProviderConnectionRepositoryUpdateInput } from "./provider-connections.js";
|
|
9
|
+
export declare const LOCAL_WORKSPACE_EXTERNAL_KEY = "self-hosted";
|
|
10
|
+
export declare function ensureLocalWorkspace(db: Kysely<Database>): Promise<WorkspaceId>;
|
|
11
|
+
export interface WorkspaceRepositories {
|
|
12
|
+
workspaceId: WorkspaceId;
|
|
13
|
+
jobs: WorkspaceJobQueue;
|
|
14
|
+
persistDecision(input: DecisionInput): Promise<PersistedDecision>;
|
|
15
|
+
recordPolicyCheck(input: {
|
|
16
|
+
decisionId: string;
|
|
17
|
+
checkRunId: string;
|
|
18
|
+
state: "in_progress" | "success" | "failure";
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
findLatestHumanReviewPolicyDecision(input: {
|
|
21
|
+
repositoryId: string;
|
|
22
|
+
pullNumber: number;
|
|
23
|
+
}): Promise<HumanReviewPolicyDecision | null>;
|
|
24
|
+
updatePolicyCheckState(input: {
|
|
25
|
+
decisionId: string;
|
|
26
|
+
state: "in_progress" | "success" | "failure";
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
markActionSucceeded(decisionId: string, at: Date): Promise<void>;
|
|
29
|
+
markActionFailed(decisionId: string, error: string, at: Date): Promise<void>;
|
|
30
|
+
readOperations(input: ReadOperationsOverviewInput): Promise<OperationsOverview>;
|
|
31
|
+
findRepositoryConfigurationTarget(repositoryId: string): Promise<RepositoryConfigurationTarget | null>;
|
|
32
|
+
recoverStaleJobs(now: Date, staleAfterMs?: number): Promise<void>;
|
|
33
|
+
applyFixedRetention(now: Date): Promise<void>;
|
|
34
|
+
acceptRoutingDelivery(input: RoutingDeliveryInput): Promise<{
|
|
35
|
+
inserted: boolean;
|
|
36
|
+
jobId: string | null;
|
|
37
|
+
}>;
|
|
38
|
+
acceptHumanReviewPolicyDelivery(input: HumanReviewPolicyDeliveryInput): Promise<{
|
|
39
|
+
inserted: boolean;
|
|
40
|
+
jobId: string | null;
|
|
41
|
+
}>;
|
|
42
|
+
activateConfiguredProviderConnection(input: ProviderConnectionMetadata): Promise<void>;
|
|
43
|
+
upsertConfiguredProviderConnection(input: ConfiguredProviderConnectionInput): Promise<void>;
|
|
44
|
+
replaceProviderConnectionRepositories(input: ConfiguredProviderConnectionInput): Promise<void>;
|
|
45
|
+
updateProviderConnectionRepositories(input: ProviderConnectionRepositoryUpdateInput): Promise<void>;
|
|
46
|
+
suspendConfiguredProviderConnection(input: ProviderConnectionMetadata): Promise<void>;
|
|
47
|
+
revokeConfiguredProviderConnection(input: Pick<ProviderConnectionMetadata, "provider" | "externalConnectionId">): Promise<void>;
|
|
48
|
+
cleanupRevokedProviderConnections(now: Date): Promise<number>;
|
|
49
|
+
}
|
|
50
|
+
export declare function createWorkspaceRepositories(db: Kysely<Database>, workspaceId: WorkspaceId): WorkspaceRepositories;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { findLatestHumanReviewPolicyDecision, markActionFailed, markActionSucceeded, persistDecision, recordPolicyCheck, updatePolicyCheckState, } from "./decisions.js";
|
|
2
|
+
import { acceptHumanReviewPolicyDelivery, acceptRoutingDelivery, } from "./deliveries.js";
|
|
3
|
+
import { createWorkspaceJobQueue, recoverStaleJobs } from "./jobs.js";
|
|
4
|
+
import { findRepositoryConfigurationTarget, readOperationsOverview, } from "./operations.js";
|
|
5
|
+
import { activateConfiguredProviderConnection, cleanupRevokedProviderConnections, replaceProviderConnectionRepositories, revokeConfiguredProviderConnection, suspendConfiguredProviderConnection, updateProviderConnectionRepositories, upsertConfiguredProviderConnection, } from "./provider-connections.js";
|
|
6
|
+
import { applyFixedRetention } from "./retention.js";
|
|
7
|
+
export const LOCAL_WORKSPACE_EXTERNAL_KEY = "self-hosted";
|
|
8
|
+
export async function ensureLocalWorkspace(db) {
|
|
9
|
+
const workspace = await db
|
|
10
|
+
.insertInto("workspaces")
|
|
11
|
+
.values({ external_key: LOCAL_WORKSPACE_EXTERNAL_KEY })
|
|
12
|
+
.onConflict((conflict) => conflict.column("external_key").doUpdateSet({
|
|
13
|
+
external_key: LOCAL_WORKSPACE_EXTERNAL_KEY,
|
|
14
|
+
}))
|
|
15
|
+
.returning("id")
|
|
16
|
+
.executeTakeFirstOrThrow();
|
|
17
|
+
return workspace.id;
|
|
18
|
+
}
|
|
19
|
+
export function createWorkspaceRepositories(db, workspaceId) {
|
|
20
|
+
return {
|
|
21
|
+
workspaceId,
|
|
22
|
+
jobs: createWorkspaceJobQueue(db, workspaceId),
|
|
23
|
+
persistDecision: (input) => persistDecision(db, workspaceId, input),
|
|
24
|
+
recordPolicyCheck: (input) => recordPolicyCheck(db, workspaceId, input),
|
|
25
|
+
findLatestHumanReviewPolicyDecision: (input) => findLatestHumanReviewPolicyDecision(db, workspaceId, input),
|
|
26
|
+
updatePolicyCheckState: (input) => updatePolicyCheckState(db, workspaceId, input),
|
|
27
|
+
markActionSucceeded: (decisionId, at) => markActionSucceeded(db, workspaceId, decisionId, at),
|
|
28
|
+
markActionFailed: (decisionId, error, at) => markActionFailed(db, workspaceId, decisionId, error, at),
|
|
29
|
+
readOperations: (input) => readOperationsOverview(db, workspaceId, input),
|
|
30
|
+
findRepositoryConfigurationTarget: (repositoryId) => findRepositoryConfigurationTarget(db, workspaceId, repositoryId),
|
|
31
|
+
recoverStaleJobs: (now, staleAfterMs) => recoverStaleJobs(db, workspaceId, now, staleAfterMs),
|
|
32
|
+
applyFixedRetention: (now) => applyFixedRetention(db, workspaceId, now),
|
|
33
|
+
acceptRoutingDelivery: (input) => acceptRoutingDelivery(db, workspaceId, input),
|
|
34
|
+
acceptHumanReviewPolicyDelivery: (input) => acceptHumanReviewPolicyDelivery(db, workspaceId, input),
|
|
35
|
+
activateConfiguredProviderConnection: (input) => activateConfiguredProviderConnection(db, workspaceId, input),
|
|
36
|
+
upsertConfiguredProviderConnection: (input) => upsertConfiguredProviderConnection(db, workspaceId, input),
|
|
37
|
+
replaceProviderConnectionRepositories: (input) => replaceProviderConnectionRepositories(db, workspaceId, input),
|
|
38
|
+
updateProviderConnectionRepositories: (input) => updateProviderConnectionRepositories(db, workspaceId, input),
|
|
39
|
+
suspendConfiguredProviderConnection: (input) => suspendConfiguredProviderConnection(db, workspaceId, input),
|
|
40
|
+
revokeConfiguredProviderConnection: (input) => revokeConfiguredProviderConnection(db, workspaceId, input),
|
|
41
|
+
cleanupRevokedProviderConnections: (now) => cleanupRevokedProviderConnections(db, workspaceId, now),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
create extension if not exists pgcrypto;
|
|
2
|
+
|
|
3
|
+
create table installations (
|
|
4
|
+
id uuid primary key default gen_random_uuid(),
|
|
5
|
+
github_installation_id bigint unique not null,
|
|
6
|
+
account_login text not null,
|
|
7
|
+
account_type text not null,
|
|
8
|
+
status text not null,
|
|
9
|
+
permissions jsonb not null default '{}'::jsonb,
|
|
10
|
+
created_at timestamptz not null default now(),
|
|
11
|
+
updated_at timestamptz not null default now()
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
create unique index one_active_installation_idx on installations (status) where status = 'active';
|
|
15
|
+
|
|
16
|
+
create table repositories (
|
|
17
|
+
id uuid primary key default gen_random_uuid(),
|
|
18
|
+
installation_id uuid not null references installations(id) on delete cascade,
|
|
19
|
+
github_repository_id bigint unique not null,
|
|
20
|
+
owner text not null,
|
|
21
|
+
name text not null,
|
|
22
|
+
default_branch text,
|
|
23
|
+
config_state text not null default 'unknown',
|
|
24
|
+
created_at timestamptz not null default now(),
|
|
25
|
+
updated_at timestamptz not null default now(),
|
|
26
|
+
last_config_mode text not null default 'shadow' check (last_config_mode in ('shadow', 'enforce'))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
create table webhook_receipts (
|
|
30
|
+
delivery_id text primary key,
|
|
31
|
+
event_name text not null,
|
|
32
|
+
installation_id bigint,
|
|
33
|
+
payload_summary jsonb not null default '{}'::jsonb,
|
|
34
|
+
created_at timestamptz not null default now()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
create table jobs (
|
|
38
|
+
id uuid primary key default gen_random_uuid(),
|
|
39
|
+
kind text not null,
|
|
40
|
+
status text not null default 'queued' check (status in ('queued', 'running', 'succeeded', 'failed')),
|
|
41
|
+
payload jsonb not null,
|
|
42
|
+
idempotency_key text not null unique,
|
|
43
|
+
attempt_count integer not null default 0,
|
|
44
|
+
max_attempts integer not null default 5,
|
|
45
|
+
run_at timestamptz not null default now(),
|
|
46
|
+
locked_at timestamptz,
|
|
47
|
+
locked_by text,
|
|
48
|
+
last_error text,
|
|
49
|
+
created_at timestamptz not null default now(),
|
|
50
|
+
updated_at timestamptz not null default now()
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
create index jobs_claim_idx on jobs (status, run_at) where status = 'queued';
|
|
54
|
+
|
|
55
|
+
create table routing_decisions (
|
|
56
|
+
id uuid primary key default gen_random_uuid(),
|
|
57
|
+
repository_id uuid references repositories(id) on delete set null,
|
|
58
|
+
delivery_id text not null,
|
|
59
|
+
action text not null,
|
|
60
|
+
risk_score integer not null,
|
|
61
|
+
selected_reviewer text,
|
|
62
|
+
no_human_reason text,
|
|
63
|
+
details jsonb not null,
|
|
64
|
+
created_at timestamptz not null default now(),
|
|
65
|
+
mode text not null default 'shadow' check (mode in ('shadow', 'enforce')),
|
|
66
|
+
action_status text not null default 'not_applied'
|
|
67
|
+
check (action_status in ('not_applied', 'pending', 'succeeded', 'failed')),
|
|
68
|
+
action_error text,
|
|
69
|
+
action_applied_at timestamptz,
|
|
70
|
+
action_failed_at timestamptz
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
create unique index routing_decisions_delivery_idx on routing_decisions (delivery_id);
|
|
74
|
+
|
|
75
|
+
create table worker_heartbeat (
|
|
76
|
+
id boolean primary key default true check (id),
|
|
77
|
+
worker_id text not null,
|
|
78
|
+
heartbeat_at timestamptz not null
|
|
79
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
alter table routing_decisions
|
|
2
|
+
add column selected_reviewers jsonb not null default '[]'::jsonb,
|
|
3
|
+
add constraint routing_decisions_selected_reviewers_array
|
|
4
|
+
check (jsonb_typeof(selected_reviewers) = 'array'),
|
|
5
|
+
add constraint routing_decisions_selected_reviewers_limit
|
|
6
|
+
check (jsonb_array_length(selected_reviewers) <= 2);
|
|
7
|
+
|
|
8
|
+
update routing_decisions
|
|
9
|
+
set selected_reviewers = jsonb_build_array(selected_reviewer)
|
|
10
|
+
where selected_reviewer is not null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
alter table routing_decisions
|
|
2
|
+
add column pull_number integer,
|
|
3
|
+
add column head_sha text,
|
|
4
|
+
add column policy_check_run_id bigint,
|
|
5
|
+
add column policy_check_state text not null default 'not_started',
|
|
6
|
+
add constraint routing_decisions_policy_check_state
|
|
7
|
+
check (policy_check_state in ('not_started', 'in_progress', 'success', 'failure'));
|
|
8
|
+
|
|
9
|
+
create index routing_decisions_human_review_policy_lookup
|
|
10
|
+
on routing_decisions (repository_id, pull_number, created_at desc);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
alter table webhook_receipts
|
|
2
|
+
add column event_action text,
|
|
3
|
+
add column hook_id text;
|
|
4
|
+
|
|
5
|
+
alter table routing_decisions
|
|
6
|
+
add column routing_key text;
|
|
7
|
+
|
|
8
|
+
update routing_decisions
|
|
9
|
+
set routing_key = 'legacy:' || delivery_id
|
|
10
|
+
where routing_key is null;
|
|
11
|
+
|
|
12
|
+
alter table routing_decisions
|
|
13
|
+
alter column routing_key set not null;
|
|
14
|
+
|
|
15
|
+
create unique index routing_decisions_routing_key_idx on routing_decisions (routing_key);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
create extension if not exists btree_gist;
|
|
2
|
+
|
|
3
|
+
create table organization_settings (
|
|
4
|
+
id boolean primary key default true check (id),
|
|
5
|
+
timezone text not null default 'UTC',
|
|
6
|
+
updated_at timestamptz not null default now()
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
insert into organization_settings (id, timezone) values (true, 'UTC');
|
|
10
|
+
|
|
11
|
+
create table reviewer_absences (
|
|
12
|
+
id uuid primary key default gen_random_uuid(),
|
|
13
|
+
reviewer_handle text not null,
|
|
14
|
+
start_at timestamptz not null,
|
|
15
|
+
end_at timestamptz not null,
|
|
16
|
+
status text not null default 'scheduled'
|
|
17
|
+
check (status in ('scheduled', 'cancelled')),
|
|
18
|
+
revision integer not null default 1 check (revision > 0),
|
|
19
|
+
cancelled_at timestamptz,
|
|
20
|
+
created_at timestamptz not null default now(),
|
|
21
|
+
updated_at timestamptz not null default now(),
|
|
22
|
+
constraint reviewer_absences_normalized_handle
|
|
23
|
+
check (reviewer_handle = lower(reviewer_handle) and reviewer_handle ~ '^@[a-z0-9_.-]+$'),
|
|
24
|
+
constraint reviewer_absences_valid_interval check (end_at > start_at),
|
|
25
|
+
constraint reviewer_absences_cancelled_at check (
|
|
26
|
+
(status = 'scheduled' and cancelled_at is null) or
|
|
27
|
+
(status = 'cancelled' and cancelled_at is not null)
|
|
28
|
+
),
|
|
29
|
+
constraint reviewer_absences_no_overlap exclude using gist (
|
|
30
|
+
reviewer_handle with =,
|
|
31
|
+
tstzrange(start_at, end_at, '[)') with &&
|
|
32
|
+
) where (status = 'scheduled')
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
create index reviewer_absences_active_lookup
|
|
36
|
+
on reviewer_absences (reviewer_handle, start_at, end_at)
|
|
37
|
+
where status = 'scheduled';
|
|
38
|
+
|
|
39
|
+
create table reviewer_replacements (
|
|
40
|
+
id uuid primary key default gen_random_uuid(),
|
|
41
|
+
absence_id uuid not null references reviewer_absences(id),
|
|
42
|
+
absence_revision integer not null check (absence_revision > 0),
|
|
43
|
+
decision_id uuid not null references routing_decisions(id) on delete cascade,
|
|
44
|
+
unavailable_reviewer text not null,
|
|
45
|
+
replacement_reviewer text,
|
|
46
|
+
outcome text not null check (outcome in (
|
|
47
|
+
'replaced', 'simulated_replacement', 'no_replacement_available',
|
|
48
|
+
'skipped_approved', 'skipped_closed', 'skipped_changed_head',
|
|
49
|
+
'skipped_policy_satisfied', 'permanent_failure'
|
|
50
|
+
)),
|
|
51
|
+
reason text not null,
|
|
52
|
+
started_at timestamptz not null,
|
|
53
|
+
completed_at timestamptz not null,
|
|
54
|
+
unique (absence_id, absence_revision, decision_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
create index reviewer_replacements_absence_history
|
|
58
|
+
on reviewer_replacements (absence_id, completed_at desc, id desc);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
create table workspaces (
|
|
2
|
+
id uuid primary key default gen_random_uuid(),
|
|
3
|
+
external_key text not null unique,
|
|
4
|
+
created_at timestamptz not null default now()
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
insert into workspaces (external_key) values ('self-hosted');
|
|
8
|
+
|
|
9
|
+
alter table installations rename to provider_connections;
|
|
10
|
+
alter table provider_connections add column workspace_id uuid;
|
|
11
|
+
alter table provider_connections add column provider text default 'github';
|
|
12
|
+
alter table provider_connections rename column github_installation_id to external_connection_id;
|
|
13
|
+
alter table provider_connections alter column external_connection_id type text using external_connection_id::text;
|
|
14
|
+
alter table provider_connections rename column account_login to workspace_login;
|
|
15
|
+
|
|
16
|
+
update provider_connections
|
|
17
|
+
set workspace_id = (select id from workspaces where external_key = 'self-hosted');
|
|
18
|
+
|
|
19
|
+
alter table provider_connections alter column workspace_id set not null;
|
|
20
|
+
alter table provider_connections alter column provider set not null;
|
|
21
|
+
alter table provider_connections alter column provider drop default;
|
|
22
|
+
alter table provider_connections
|
|
23
|
+
add constraint provider_connections_workspace_fkey
|
|
24
|
+
foreign key (workspace_id) references workspaces(id) on delete cascade;
|
|
25
|
+
alter table provider_connections
|
|
26
|
+
add constraint provider_connections_workspace_id_key unique (workspace_id, id);
|
|
27
|
+
alter table provider_connections
|
|
28
|
+
add constraint provider_connections_workspace_provider_id_key unique (workspace_id, provider, id);
|
|
29
|
+
alter table provider_connections drop constraint installations_github_installation_id_key;
|
|
30
|
+
alter table provider_connections
|
|
31
|
+
add constraint provider_connections_workspace_external_key
|
|
32
|
+
unique (workspace_id, provider, external_connection_id);
|
|
33
|
+
drop index one_active_installation_idx;
|
|
34
|
+
create unique index one_active_provider_connection_idx
|
|
35
|
+
on provider_connections (workspace_id, status) where status = 'active';
|
|
36
|
+
|
|
37
|
+
alter table repositories add column workspace_id uuid;
|
|
38
|
+
alter table repositories add column provider text default 'github';
|
|
39
|
+
alter table repositories rename column installation_id to provider_connection_id;
|
|
40
|
+
alter table repositories rename column github_repository_id to external_repository_id;
|
|
41
|
+
alter table repositories alter column external_repository_id type text using external_repository_id::text;
|
|
42
|
+
|
|
43
|
+
update repositories repositories
|
|
44
|
+
set workspace_id = provider_connections.workspace_id,
|
|
45
|
+
provider = provider_connections.provider
|
|
46
|
+
from provider_connections
|
|
47
|
+
where provider_connections.id = repositories.provider_connection_id;
|
|
48
|
+
|
|
49
|
+
alter table repositories alter column workspace_id set not null;
|
|
50
|
+
alter table repositories alter column provider set not null;
|
|
51
|
+
alter table repositories alter column provider drop default;
|
|
52
|
+
alter table repositories drop constraint repositories_installation_id_fkey;
|
|
53
|
+
alter table repositories drop constraint repositories_github_repository_id_key;
|
|
54
|
+
alter table repositories
|
|
55
|
+
add constraint repositories_workspace_fkey
|
|
56
|
+
foreign key (workspace_id) references workspaces(id) on delete cascade;
|
|
57
|
+
alter table repositories
|
|
58
|
+
add constraint repositories_workspace_provider_connection_fkey
|
|
59
|
+
foreign key (workspace_id, provider, provider_connection_id)
|
|
60
|
+
references provider_connections(workspace_id, provider, id) on delete cascade;
|
|
61
|
+
alter table repositories
|
|
62
|
+
add constraint repositories_workspace_id_key unique (workspace_id, id);
|
|
63
|
+
alter table repositories
|
|
64
|
+
add constraint repositories_workspace_provider_repository_key
|
|
65
|
+
unique (workspace_id, provider, external_repository_id);
|
|
66
|
+
|
|
67
|
+
alter table webhook_receipts add column id uuid default gen_random_uuid();
|
|
68
|
+
alter table webhook_receipts add column workspace_id uuid;
|
|
69
|
+
alter table webhook_receipts add column provider text default 'github';
|
|
70
|
+
alter table webhook_receipts rename column installation_id to external_connection_id;
|
|
71
|
+
alter table webhook_receipts alter column external_connection_id type text using external_connection_id::text;
|
|
72
|
+
|
|
73
|
+
update webhook_receipts
|
|
74
|
+
set workspace_id = (select id from workspaces where external_key = 'self-hosted');
|
|
75
|
+
|
|
76
|
+
alter table webhook_receipts alter column id set not null;
|
|
77
|
+
alter table webhook_receipts alter column workspace_id set not null;
|
|
78
|
+
alter table webhook_receipts alter column provider set not null;
|
|
79
|
+
alter table webhook_receipts alter column provider drop default;
|
|
80
|
+
alter table webhook_receipts drop constraint webhook_receipts_pkey;
|
|
81
|
+
alter table webhook_receipts add constraint webhook_receipts_pkey primary key (id);
|
|
82
|
+
alter table webhook_receipts
|
|
83
|
+
add constraint webhook_receipts_workspace_fkey
|
|
84
|
+
foreign key (workspace_id) references workspaces(id) on delete cascade;
|
|
85
|
+
alter table webhook_receipts
|
|
86
|
+
add constraint webhook_receipts_workspace_provider_delivery_key
|
|
87
|
+
unique (workspace_id, provider, delivery_id);
|
|
88
|
+
|
|
89
|
+
alter table jobs add column workspace_id uuid;
|
|
90
|
+
alter table jobs add column provider text default 'github';
|
|
91
|
+
alter table jobs add column provider_connection_id uuid;
|
|
92
|
+
|
|
93
|
+
update jobs jobs
|
|
94
|
+
set workspace_id = (select id from workspaces where external_key = 'self-hosted'),
|
|
95
|
+
provider = coalesce(jobs.payload -> 'changeRequest' -> 'repository' ->> 'provider', 'github'),
|
|
96
|
+
provider_connection_id = provider_connections.id
|
|
97
|
+
from provider_connections
|
|
98
|
+
where provider_connections.workspace_id = (select id from workspaces where external_key = 'self-hosted')
|
|
99
|
+
and provider_connections.external_connection_id = jobs.payload ->> 'providerConnectionId';
|
|
100
|
+
|
|
101
|
+
update jobs jobs
|
|
102
|
+
set workspace_id = (select id from workspaces where external_key = 'self-hosted'),
|
|
103
|
+
provider = provider_connections.provider,
|
|
104
|
+
provider_connection_id = provider_connections.id
|
|
105
|
+
from provider_connections
|
|
106
|
+
where jobs.provider_connection_id is null
|
|
107
|
+
and provider_connections.workspace_id = (select id from workspaces where external_key = 'self-hosted')
|
|
108
|
+
and provider_connections.id = (
|
|
109
|
+
select candidate.id
|
|
110
|
+
from provider_connections candidate
|
|
111
|
+
where candidate.workspace_id = provider_connections.workspace_id
|
|
112
|
+
order by (candidate.status = 'active') desc, candidate.created_at, candidate.id
|
|
113
|
+
limit 1
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
insert into provider_connections (
|
|
117
|
+
workspace_id, provider, external_connection_id, workspace_login, account_type, status, permissions
|
|
118
|
+
)
|
|
119
|
+
select workspaces.id, 'github', 'legacy', 'self-hosted', 'Organization', 'active', '{}'::jsonb
|
|
120
|
+
from workspaces
|
|
121
|
+
where workspaces.external_key = 'self-hosted'
|
|
122
|
+
and exists (select 1 from jobs where provider_connection_id is null)
|
|
123
|
+
and not exists (
|
|
124
|
+
select 1 from provider_connections where provider_connections.workspace_id = workspaces.id
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
update jobs jobs
|
|
128
|
+
set workspace_id = provider_connections.workspace_id,
|
|
129
|
+
provider = provider_connections.provider,
|
|
130
|
+
provider_connection_id = provider_connections.id
|
|
131
|
+
from provider_connections
|
|
132
|
+
where jobs.provider_connection_id is null
|
|
133
|
+
and provider_connections.external_connection_id = 'legacy';
|
|
134
|
+
|
|
135
|
+
alter table jobs alter column workspace_id set not null;
|
|
136
|
+
alter table jobs alter column provider set not null;
|
|
137
|
+
alter table jobs alter column provider drop default;
|
|
138
|
+
alter table jobs alter column provider_connection_id set not null;
|
|
139
|
+
alter table jobs drop constraint jobs_idempotency_key_key;
|
|
140
|
+
alter table jobs
|
|
141
|
+
add constraint jobs_workspace_fkey
|
|
142
|
+
foreign key (workspace_id) references workspaces(id) on delete cascade;
|
|
143
|
+
alter table jobs
|
|
144
|
+
add constraint jobs_workspace_provider_connection_fkey
|
|
145
|
+
foreign key (workspace_id, provider, provider_connection_id)
|
|
146
|
+
references provider_connections(workspace_id, provider, id) on delete cascade;
|
|
147
|
+
alter table jobs
|
|
148
|
+
add constraint jobs_workspace_id_key unique (workspace_id, id);
|
|
149
|
+
alter table jobs
|
|
150
|
+
add constraint jobs_workspace_idempotency_key unique (workspace_id, idempotency_key);
|
|
151
|
+
|
|
152
|
+
alter table routing_decisions add column workspace_id uuid;
|
|
153
|
+
alter table routing_decisions add column organization_config_version text;
|
|
154
|
+
alter table routing_decisions add column repository_config_path text;
|
|
155
|
+
alter table routing_decisions add column repository_config_revision text;
|
|
156
|
+
alter table routing_decisions add column effective_config_hash text;
|
|
157
|
+
alter table routing_decisions add column inheritance_mode text;
|
|
158
|
+
alter table routing_decisions add column config_diagnostics jsonb not null default '[]'::jsonb;
|
|
159
|
+
alter table routing_decisions add column config_sources jsonb not null default '{}'::jsonb;
|
|
160
|
+
|
|
161
|
+
update routing_decisions decisions
|
|
162
|
+
set workspace_id = coalesce(
|
|
163
|
+
repositories.workspace_id,
|
|
164
|
+
(select id from workspaces where external_key = 'self-hosted')
|
|
165
|
+
),
|
|
166
|
+
effective_config_hash = encode(digest(decisions.details::text, 'sha256'), 'hex'),
|
|
167
|
+
inheritance_mode = 'legacy'
|
|
168
|
+
from repositories
|
|
169
|
+
where repositories.id = decisions.repository_id;
|
|
170
|
+
|
|
171
|
+
update routing_decisions decisions
|
|
172
|
+
set workspace_id = (select id from workspaces where external_key = 'self-hosted'),
|
|
173
|
+
effective_config_hash = encode(digest(decisions.details::text, 'sha256'), 'hex'),
|
|
174
|
+
inheritance_mode = 'legacy'
|
|
175
|
+
where decisions.workspace_id is null;
|
|
176
|
+
|
|
177
|
+
alter table routing_decisions alter column workspace_id set not null;
|
|
178
|
+
alter table routing_decisions alter column effective_config_hash set not null;
|
|
179
|
+
alter table routing_decisions alter column inheritance_mode set not null;
|
|
180
|
+
alter table routing_decisions
|
|
181
|
+
add constraint routing_decisions_inheritance_mode_check
|
|
182
|
+
check (inheritance_mode in ('legacy', 'defaults', 'organization', 'replace', 'inherit'));
|
|
183
|
+
alter table routing_decisions drop constraint routing_decisions_repository_id_fkey;
|
|
184
|
+
alter table routing_decisions
|
|
185
|
+
add constraint routing_decisions_workspace_fkey
|
|
186
|
+
foreign key (workspace_id) references workspaces(id) on delete cascade;
|
|
187
|
+
alter table routing_decisions
|
|
188
|
+
add constraint routing_decisions_workspace_repository_fkey
|
|
189
|
+
foreign key (workspace_id, repository_id)
|
|
190
|
+
references repositories(workspace_id, id) on delete set null (repository_id);
|
|
191
|
+
alter table routing_decisions
|
|
192
|
+
add constraint routing_decisions_workspace_id_key unique (workspace_id, id);
|
|
193
|
+
drop index routing_decisions_delivery_idx;
|
|
194
|
+
create unique index routing_decisions_workspace_delivery_idx
|
|
195
|
+
on routing_decisions (workspace_id, delivery_id);
|
|
196
|
+
drop index routing_decisions_routing_key_idx;
|
|
197
|
+
create unique index routing_decisions_workspace_routing_key_idx
|
|
198
|
+
on routing_decisions (workspace_id, routing_key);
|