Skip to content

Commit dd06c70

Browse files
authored
Merge pull request #2513 from divaltor/docker-image-tag
feat(tags): Add support for tags from Github Packages
2 parents 4518ea2 + 4d36741 commit dd06c70

File tree

3 files changed

+504
-38
lines changed

3 files changed

+504
-38
lines changed

apps/dokploy/__test__/deploy/github.test.ts

Lines changed: 311 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { describe, expect, it } from "vitest";
2-
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
2+
import {
3+
extractCommitMessage,
4+
extractImageName,
5+
extractImageTag,
6+
extractImageTagFromRequest,
7+
} from "@/pages/api/deploy/[refreshToken]";
38

49
describe("GitHub Webhook Skip CI", () => {
510
const mockGithubHeaders = {
@@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => {
96101
);
97102
});
98103
});
104+
105+
describe("GitHub Packages Docker Image Tag Extraction", () => {
106+
it("should extract tag from container_metadata", () => {
107+
const headers = { "x-github-event": "registry_package" };
108+
const body = {
109+
registry_package: {
110+
package_version: {
111+
version: "sha256:abc123...",
112+
container_metadata: {
113+
tag: {
114+
name: "v1.0.0",
115+
digest: "sha256:abc123...",
116+
},
117+
},
118+
package_url: "ghcr.io/owner/repo:v1.0.0",
119+
},
120+
},
121+
};
122+
123+
const tag = extractImageTagFromRequest(headers, body);
124+
expect(tag).toBe("v1.0.0");
125+
});
126+
127+
it("should extract tag from package_url when container_metadata tag matches version", () => {
128+
const headers = { "x-github-event": "registry_package" };
129+
const body = {
130+
registry_package: {
131+
package_version: {
132+
version: "sha256:abc123...",
133+
container_metadata: {
134+
tag: {
135+
name: "sha256:abc123...",
136+
digest: "sha256:abc123...",
137+
},
138+
},
139+
package_url: "ghcr.io/owner/repo:latest",
140+
},
141+
},
142+
};
143+
144+
const tag = extractImageTagFromRequest(headers, body);
145+
expect(tag).toBe("latest");
146+
});
147+
148+
it("should extract tag from package_url when container_metadata is missing", () => {
149+
const headers = { "x-github-event": "registry_package" };
150+
const body = {
151+
registry_package: {
152+
package_version: {
153+
version: "sha256:abc123...",
154+
package_url: "ghcr.io/owner/repo:1.2.3",
155+
},
156+
},
157+
};
158+
159+
const tag = extractImageTagFromRequest(headers, body);
160+
expect(tag).toBe("1.2.3");
161+
});
162+
163+
it("should handle different tag formats in package_url", () => {
164+
const headers = { "x-github-event": "registry_package" };
165+
const testCases = [
166+
{ url: "ghcr.io/owner/repo:latest", expected: "latest" },
167+
{ url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" },
168+
{ url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" },
169+
{ url: "ghcr.io/owner/repo:dev", expected: "dev" },
170+
];
171+
172+
for (const testCase of testCases) {
173+
const body = {
174+
registry_package: {
175+
package_version: {
176+
version: "sha256:abc123...",
177+
package_url: testCase.url,
178+
},
179+
},
180+
};
181+
182+
const tag = extractImageTagFromRequest(headers, body);
183+
expect(tag).toBe(testCase.expected);
184+
}
185+
});
186+
187+
it("should return null for non-registry_package events", () => {
188+
const headers = { "x-github-event": "push" };
189+
const body = {
190+
registry_package: {
191+
package_version: {
192+
package_url: "ghcr.io/owner/repo:latest",
193+
},
194+
},
195+
};
196+
197+
const tag = extractImageTagFromRequest(headers, body);
198+
expect(tag).toBeNull();
199+
});
200+
201+
it("should return null when package_version is missing", () => {
202+
const headers = { "x-github-event": "registry_package" };
203+
const body = {
204+
registry_package: {},
205+
};
206+
207+
const tag = extractImageTagFromRequest(headers, body);
208+
expect(tag).toBeNull();
209+
});
210+
211+
it("should return null when package_url has no tag", () => {
212+
const headers = { "x-github-event": "registry_package" };
213+
const body = {
214+
registry_package: {
215+
package_version: {
216+
version: "sha256:abc123...",
217+
package_url: "ghcr.io/owner/repo",
218+
},
219+
},
220+
};
221+
222+
const tag = extractImageTagFromRequest(headers, body);
223+
expect(tag).toBeNull();
224+
});
225+
226+
it("should return null when package_url ends with colon (no tag)", () => {
227+
const headers = { "x-github-event": "registry_package" };
228+
const body = {
229+
registry_package: {
230+
package_version: {
231+
version: "sha256:abc123...",
232+
package_url: "ghcr.io/owner/repo:",
233+
container_metadata: {
234+
tag: {
235+
name: "",
236+
digest: "sha256:abc123...",
237+
},
238+
},
239+
},
240+
},
241+
};
242+
243+
const tag = extractImageTagFromRequest(headers, body);
244+
expect(tag).toBeNull();
245+
});
246+
247+
it("should return null when tag name is empty string", () => {
248+
const headers = { "x-github-event": "registry_package" };
249+
const body = {
250+
registry_package: {
251+
package_version: {
252+
version: "sha256:abc123...",
253+
container_metadata: {
254+
tag: {
255+
name: "",
256+
digest: "sha256:abc123...",
257+
},
258+
},
259+
package_url: "ghcr.io/owner/repo:",
260+
},
261+
},
262+
};
263+
264+
const tag = extractImageTagFromRequest(headers, body);
265+
expect(tag).toBeNull();
266+
});
267+
268+
it("should ignore tag if it matches the version (digest)", () => {
269+
const headers = { "x-github-event": "registry_package" };
270+
const body = {
271+
registry_package: {
272+
package_version: {
273+
version: "sha256:abc123...",
274+
container_metadata: {
275+
tag: {
276+
name: "sha256:abc123...",
277+
digest: "sha256:abc123...",
278+
},
279+
},
280+
package_url: "ghcr.io/owner/repo:latest",
281+
},
282+
},
283+
};
284+
285+
const tag = extractImageTagFromRequest(headers, body);
286+
expect(tag).toBe("latest");
287+
});
288+
289+
it("should handle registry_package commit message with package_url", () => {
290+
const headers = { "x-github-event": "registry_package" };
291+
const body = {
292+
registry_package: {
293+
package_version: {
294+
package_url: "ghcr.io/owner/repo:latest",
295+
},
296+
},
297+
};
298+
299+
const message = extractCommitMessage(headers, body);
300+
expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest");
301+
});
302+
303+
it("should handle registry_package commit message when package_url is missing", () => {
304+
const headers = { "x-github-event": "registry_package" };
305+
const body = {
306+
registry_package: {
307+
package_version: {
308+
version: "sha256:abc123...",
309+
},
310+
},
311+
};
312+
313+
const message = extractCommitMessage(headers, body);
314+
expect(message).toBe("Docker GHCR image pushed");
315+
});
316+
317+
it("should handle registry_package commit message when package_version is missing", () => {
318+
const headers = { "x-github-event": "registry_package" };
319+
const body = {
320+
registry_package: {},
321+
};
322+
323+
const message = extractCommitMessage(headers, body);
324+
expect(message).toBe("NEW COMMIT");
325+
});
326+
});
327+
328+
describe("Docker Image Name and Tag Extraction", () => {
329+
describe("extractImageName", () => {
330+
it("should return image name without tag", () => {
331+
expect(extractImageName("my-image:latest")).toBe("my-image");
332+
expect(extractImageName("my-image:1.0.0")).toBe("my-image");
333+
expect(extractImageName("ghcr.io/owner/repo:latest")).toBe(
334+
"ghcr.io/owner/repo",
335+
);
336+
});
337+
338+
it("should return full image name when no tag is present", () => {
339+
expect(extractImageName("my-image")).toBe("my-image");
340+
expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo");
341+
});
342+
343+
it("should handle images with port numbers correctly", () => {
344+
expect(extractImageName("registry:5000/image:tag")).toBe(
345+
"registry:5000/image",
346+
);
347+
expect(extractImageName("localhost:5000/my-app:latest")).toBe(
348+
"localhost:5000/my-app",
349+
);
350+
});
351+
352+
it("should handle complex image paths", () => {
353+
expect(
354+
extractImageName("myregistryhost:5000/fedora/httpd:version1.0"),
355+
).toBe("myregistryhost:5000/fedora/httpd");
356+
expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe(
357+
"registry.example.com:8080/ns/app",
358+
);
359+
});
360+
361+
it("should return null for invalid inputs", () => {
362+
expect(extractImageName(null)).toBeNull();
363+
expect(extractImageName("")).toBeNull();
364+
});
365+
366+
it("should handle edge cases with multiple colons", () => {
367+
expect(extractImageName("image:tag:extra")).toBe("image:tag");
368+
expect(extractImageName("registry:5000:invalid")).toBe("registry:5000");
369+
});
370+
});
371+
372+
describe("extractImageTag", () => {
373+
it("should extract tag from image with tag", () => {
374+
expect(extractImageTag("my-image:latest")).toBe("latest");
375+
expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0");
376+
expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3");
377+
});
378+
379+
it("should return 'latest' when no tag is present", () => {
380+
expect(extractImageTag("my-image")).toBe("latest");
381+
expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest");
382+
});
383+
384+
it("should handle complex image paths with tags", () => {
385+
expect(
386+
extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"),
387+
).toBe("version1.0");
388+
expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe(
389+
"v1.2.3",
390+
);
391+
});
392+
393+
it("should return null for invalid inputs", () => {
394+
expect(extractImageTag(null)).toBeNull();
395+
expect(extractImageTag("")).toBeNull();
396+
});
397+
398+
it("should handle edge cases with multiple colons", () => {
399+
expect(extractImageTag("image:tag:extra")).toBe("extra");
400+
expect(extractImageTag("registry:5000/image:tag")).toBe("tag");
401+
});
402+
403+
it("should handle numeric tags", () => {
404+
expect(extractImageTag("my-image:123")).toBe("123");
405+
expect(extractImageTag("my-image:1")).toBe("1");
406+
});
407+
});
408+
});

apps/dokploy/components/dashboard/settings/users/add-permissions.tsx

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { findEnvironmentById } from "@dokploy/server/index";
21
import { zodResolver } from "@hookform/resolvers/zod";
32
import { useEffect, useState } from "react";
43
import { useForm } from "react-hook-form";
@@ -27,12 +26,10 @@ import {
2726
FormMessage,
2827
} from "@/components/ui/form";
2928
import { Switch } from "@/components/ui/switch";
30-
import { api } from "@/utils/api";
29+
import { api, type RouterOutputs } from "@/utils/api";
3130

32-
type Environment = Omit<
33-
Awaited<ReturnType<typeof findEnvironmentById>>,
34-
"project"
35-
>;
31+
type Project = RouterOutputs["project"]["all"][number];
32+
type Environment = Project["environments"][number];
3633

3734
export type Services = {
3835
appName: string;
@@ -53,17 +50,16 @@ export type Services = {
5350
};
5451

5552
export const extractServices = (data: Environment | undefined) => {
56-
const applications: Services[] =
57-
data?.applications.map((item) => ({
58-
appName: item.appName,
59-
name: item.name,
60-
type: "application",
61-
id: item.applicationId,
62-
createdAt: item.createdAt,
63-
status: item.applicationStatus,
64-
description: item.description,
65-
serverId: item.serverId,
66-
})) || [];
53+
const applications: Services[] = (data?.applications?.map((item) => ({
54+
appName: item.appName,
55+
name: item.name,
56+
type: "application",
57+
id: item.applicationId,
58+
createdAt: item.createdAt,
59+
status: item.applicationStatus,
60+
description: item.description,
61+
serverId: item.serverId,
62+
})) ?? []) as Services[];
6763

6864
const mariadb: Services[] =
6965
data?.mariadb.map((item) => ({
@@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => {
125121
serverId: item.serverId,
126122
})) || [];
127123

128-
const compose: Services[] =
129-
data?.compose.map((item) => ({
130-
appName: item.appName,
131-
name: item.name,
132-
type: "compose",
133-
id: item.composeId,
134-
createdAt: item.createdAt,
135-
status: item.composeStatus,
136-
description: item.description,
137-
serverId: item.serverId,
138-
})) || [];
124+
const compose: Services[] = (data?.compose?.map((item) => ({
125+
appName: item.appName,
126+
name: item.name,
127+
type: "compose",
128+
id: item.composeId,
129+
createdAt: item.createdAt,
130+
status: item.composeStatus,
131+
description: item.description,
132+
serverId: item.serverId,
133+
})) ?? []) as Services[];
139134

140135
applications.push(
141136
...mysql,

0 commit comments

Comments
 (0)