mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(git): push webhook receiver + repo connect on service create
New public POST /api/git/webhooks/🆔 verifies provider signatures (GitHub/Gitea HMAC-SHA256, GitLab token), filters branch pushes, matches connected repo to services by clone_url/full_name + branch, inserts deployment rows and enqueues runDeploymentAndSync (trigger=webhook). Service-create dialog now connects the selected provider repo and registers a push webhook (409-tolerant via connected-repo lookup). api-client: connectGitRepository, listConnectedGitRepositories, createGitWebhook typed from generated schemas. GitWebhook spec schema corrected (events array + branch field). Authored By: TDvorak <[email protected]>
This commit is contained in:
1 parent
93e3074477
commit
25703592c6
8 files changed
+550
-11
No files matched your search
@@ -2,8 +2,12 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
ApiError,
|
||||
connectGitRepository,
|
||||
createGitWebhook,
|
||||
createService,
|
||||
getProjectById,
|
||||
listConnectedGitRepositories,
|
||||
listGitBranches,
|
||||
listGitProviders,
|
||||
listGitRepositories,
|
||||
@@ -50,10 +54,15 @@ const viewItems: Array<{ key: WorkspaceView; label: string; icon: typeof LayoutG
|
||||
const serviceTypes: Array<CreateServiceInput['type']> = ['web', 'worker', 'database', 'cron'];
|
||||
const serviceEnvironments = ['production', 'preview', 'development'] as const;
|
||||
|
||||
type CreateServiceSubmit = CreateServiceInput & {
|
||||
gitProviderId?: string;
|
||||
gitRepoFullName?: string;
|
||||
};
|
||||
|
||||
function ServiceCreateDialog(props: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: CreateServiceInput) => void;
|
||||
onSubmit: (payload: CreateServiceSubmit) => void;
|
||||
loading: boolean;
|
||||
errorMessage?: string;
|
||||
}) {
|
||||
@@ -365,6 +374,8 @@ function ServiceCreateDialog(props: {
|
||||
git_repo: source === 'git' ? form.git_repo?.trim() : '',
|
||||
git_branch: source === 'git' ? form.git_branch?.trim() : '',
|
||||
build_path: source === 'git' ? form.build_path?.trim() : '',
|
||||
gitProviderId: source === 'git' && repoFullName ? providerId : undefined,
|
||||
gitRepoFullName: source === 'git' ? repoFullName || undefined : undefined,
|
||||
})
|
||||
}
|
||||
className="px-5 py-2 rounded-[var(--radius-md)] text-[var(--accent-on)] text-sm font-medium shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
@@ -416,7 +427,35 @@ export function ProjectWorkspacePage() {
|
||||
});
|
||||
|
||||
const createServiceMutation = useMutation({
|
||||
mutationFn: (payload: CreateServiceInput) => createService(projectId, payload),
|
||||
mutationFn: async (payload: CreateServiceSubmit) => {
|
||||
const { gitProviderId, gitRepoFullName, ...serviceInput } = payload;
|
||||
const service = await createService(projectId, serviceInput);
|
||||
if (gitProviderId && gitRepoFullName) {
|
||||
let repoId: string | undefined;
|
||||
try {
|
||||
const repo = await connectGitRepository({
|
||||
provider_id: gitProviderId,
|
||||
repo_full_name: gitRepoFullName,
|
||||
});
|
||||
repoId = repo.id;
|
||||
} catch (err) {
|
||||
if (!(err instanceof ApiError && err.status === 409)) {
|
||||
throw err;
|
||||
}
|
||||
repoId = (await listConnectedGitRepositories()).find(
|
||||
(r) => r.full_name === gitRepoFullName,
|
||||
)?.id;
|
||||
}
|
||||
if (repoId) {
|
||||
await createGitWebhook({
|
||||
repo_id: repoId,
|
||||
events: ['push'],
|
||||
branch: payload.git_branch ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
return service;
|
||||
},
|
||||
onSuccess: () => {
|
||||
setCreateOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['project-services', projectId] });
|
||||
|
||||
@@ -4748,6 +4748,92 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/git/webhooks/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Push event receiver
|
||||
* @description Public provider webhook endpoint. The HMAC signature against the stored webhook secret replaces session auth: GitHub X-Hub-Signature-256, Gitea X-Gitea-Signature, GitLab X-Gitlab-Token. Matching branch pushes enqueue deployments for connected services.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Push accepted; matched services queued for deployment */
|
||||
202: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
received?: boolean;
|
||||
branch?: string;
|
||||
deployments?: number;
|
||||
ignored?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Invalid webhook ID or payload */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid signature */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
/** @description Webhook disabled */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
/** @description Webhook or repository not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/agents/{id}/commands/{commandId}/result": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -7014,8 +7100,10 @@ export interface components {
|
||||
id?: string;
|
||||
repo_id?: string;
|
||||
provider_id?: string;
|
||||
/** @description JSON-encoded array of subscribed events */
|
||||
events?: string;
|
||||
/** @description Subscribed provider events */
|
||||
events?: string[];
|
||||
/** @description Branch filter for push events */
|
||||
branch?: string;
|
||||
active?: boolean;
|
||||
/** Format: date-time */
|
||||
created_at?: string;
|
||||
|
||||
@@ -1230,3 +1230,36 @@ export async function listGitBranches(
|
||||
);
|
||||
return payload.branches ?? [];
|
||||
}
|
||||
|
||||
export type ConnectGitRepositoryInput = components['schemas']['ConnectGitRepoRequest'];
|
||||
export type CreateGitWebhookInput = components['schemas']['CreateWebhookRequest'];
|
||||
export type GitWebhookEntity = components['schemas']['GitWebhook'];
|
||||
|
||||
export async function connectGitRepository(
|
||||
input: ConnectGitRepositoryInput,
|
||||
): Promise<GitRepositoryEntity> {
|
||||
const payload = await requestJson<{ repository?: GitRepositoryEntity }>(
|
||||
'/git/repositories/connect',
|
||||
{ method: 'POST', body: JSON.stringify(input) },
|
||||
);
|
||||
if (!payload.repository?.id) {
|
||||
throw new ApiError('Connect repository response is invalid', 500);
|
||||
}
|
||||
return payload.repository;
|
||||
}
|
||||
|
||||
export async function listConnectedGitRepositories(): Promise<GitRepositoryEntity[]> {
|
||||
const payload = await requestJson<{ repositories?: GitRepositoryEntity[] }>('/git/repositories');
|
||||
return payload.repositories ?? [];
|
||||
}
|
||||
|
||||
export async function createGitWebhook(input: CreateGitWebhookInput): Promise<GitWebhookEntity> {
|
||||
const payload = await requestJson<{ webhook?: GitWebhookEntity }>('/git/webhooks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!payload.webhook?.id) {
|
||||
throw new ApiError('Webhook response is invalid', 500);
|
||||
}
|
||||
return payload.webhook;
|
||||
}
|
||||
Reference in new issue
Block a user