mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-02 10:12:15 +00:00
Merge pull request #2513 from divaltor/docker-image-tag
feat(tags): Add support for tags from Github Packages
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
import {
|
||||||
|
extractCommitMessage,
|
||||||
|
extractImageName,
|
||||||
|
extractImageTag,
|
||||||
|
extractImageTagFromRequest,
|
||||||
|
} from "@/pages/api/deploy/[refreshToken]";
|
||||||
|
|
||||||
describe("GitHub Webhook Skip CI", () => {
|
describe("GitHub Webhook Skip CI", () => {
|
||||||
const mockGithubHeaders = {
|
const mockGithubHeaders = {
|
||||||
@@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("GitHub Packages Docker Image Tag Extraction", () => {
|
||||||
|
it("should extract tag from container_metadata", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
container_metadata: {
|
||||||
|
tag: {
|
||||||
|
name: "v1.0.0",
|
||||||
|
digest: "sha256:abc123...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
package_url: "ghcr.io/owner/repo:v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBe("v1.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract tag from package_url when container_metadata tag matches version", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
container_metadata: {
|
||||||
|
tag: {
|
||||||
|
name: "sha256:abc123...",
|
||||||
|
digest: "sha256:abc123...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
package_url: "ghcr.io/owner/repo:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBe("latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract tag from package_url when container_metadata is missing", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
package_url: "ghcr.io/owner/repo:1.2.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBe("1.2.3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle different tag formats in package_url", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const testCases = [
|
||||||
|
{ url: "ghcr.io/owner/repo:latest", expected: "latest" },
|
||||||
|
{ url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" },
|
||||||
|
{ url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" },
|
||||||
|
{ url: "ghcr.io/owner/repo:dev", expected: "dev" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
package_url: testCase.url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBe(testCase.expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for non-registry_package events", () => {
|
||||||
|
const headers = { "x-github-event": "push" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
package_url: "ghcr.io/owner/repo:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when package_version is missing", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when package_url has no tag", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
package_url: "ghcr.io/owner/repo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when package_url ends with colon (no tag)", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
package_url: "ghcr.io/owner/repo:",
|
||||||
|
container_metadata: {
|
||||||
|
tag: {
|
||||||
|
name: "",
|
||||||
|
digest: "sha256:abc123...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when tag name is empty string", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
container_metadata: {
|
||||||
|
tag: {
|
||||||
|
name: "",
|
||||||
|
digest: "sha256:abc123...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
package_url: "ghcr.io/owner/repo:",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore tag if it matches the version (digest)", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
container_metadata: {
|
||||||
|
tag: {
|
||||||
|
name: "sha256:abc123...",
|
||||||
|
digest: "sha256:abc123...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
package_url: "ghcr.io/owner/repo:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = extractImageTagFromRequest(headers, body);
|
||||||
|
expect(tag).toBe("latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle registry_package commit message with package_url", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
package_url: "ghcr.io/owner/repo:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = extractCommitMessage(headers, body);
|
||||||
|
expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle registry_package commit message when package_url is missing", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {
|
||||||
|
package_version: {
|
||||||
|
version: "sha256:abc123...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = extractCommitMessage(headers, body);
|
||||||
|
expect(message).toBe("Docker GHCR image pushed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle registry_package commit message when package_version is missing", () => {
|
||||||
|
const headers = { "x-github-event": "registry_package" };
|
||||||
|
const body = {
|
||||||
|
registry_package: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = extractCommitMessage(headers, body);
|
||||||
|
expect(message).toBe("NEW COMMIT");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Docker Image Name and Tag Extraction", () => {
|
||||||
|
describe("extractImageName", () => {
|
||||||
|
it("should return image name without tag", () => {
|
||||||
|
expect(extractImageName("my-image:latest")).toBe("my-image");
|
||||||
|
expect(extractImageName("my-image:1.0.0")).toBe("my-image");
|
||||||
|
expect(extractImageName("ghcr.io/owner/repo:latest")).toBe(
|
||||||
|
"ghcr.io/owner/repo",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return full image name when no tag is present", () => {
|
||||||
|
expect(extractImageName("my-image")).toBe("my-image");
|
||||||
|
expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle images with port numbers correctly", () => {
|
||||||
|
expect(extractImageName("registry:5000/image:tag")).toBe(
|
||||||
|
"registry:5000/image",
|
||||||
|
);
|
||||||
|
expect(extractImageName("localhost:5000/my-app:latest")).toBe(
|
||||||
|
"localhost:5000/my-app",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex image paths", () => {
|
||||||
|
expect(
|
||||||
|
extractImageName("myregistryhost:5000/fedora/httpd:version1.0"),
|
||||||
|
).toBe("myregistryhost:5000/fedora/httpd");
|
||||||
|
expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe(
|
||||||
|
"registry.example.com:8080/ns/app",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for invalid inputs", () => {
|
||||||
|
expect(extractImageName(null)).toBeNull();
|
||||||
|
expect(extractImageName("")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle edge cases with multiple colons", () => {
|
||||||
|
expect(extractImageName("image:tag:extra")).toBe("image:tag");
|
||||||
|
expect(extractImageName("registry:5000:invalid")).toBe("registry:5000");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractImageTag", () => {
|
||||||
|
it("should extract tag from image with tag", () => {
|
||||||
|
expect(extractImageTag("my-image:latest")).toBe("latest");
|
||||||
|
expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0");
|
||||||
|
expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'latest' when no tag is present", () => {
|
||||||
|
expect(extractImageTag("my-image")).toBe("latest");
|
||||||
|
expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex image paths with tags", () => {
|
||||||
|
expect(
|
||||||
|
extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"),
|
||||||
|
).toBe("version1.0");
|
||||||
|
expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe(
|
||||||
|
"v1.2.3",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for invalid inputs", () => {
|
||||||
|
expect(extractImageTag(null)).toBeNull();
|
||||||
|
expect(extractImageTag("")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle edge cases with multiple colons", () => {
|
||||||
|
expect(extractImageTag("image:tag:extra")).toBe("extra");
|
||||||
|
expect(extractImageTag("registry:5000/image:tag")).toBe("tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle numeric tags", () => {
|
||||||
|
expect(extractImageTag("my-image:123")).toBe("123");
|
||||||
|
expect(extractImageTag("my-image:1")).toBe("1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { findEnvironmentById } from "@dokploy/server/index";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -27,12 +26,10 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
|
|
||||||
type Environment = Omit<
|
type Project = RouterOutputs["project"]["all"][number];
|
||||||
Awaited<ReturnType<typeof findEnvironmentById>>,
|
type Environment = Project["environments"][number];
|
||||||
"project"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -53,17 +50,16 @@ export type Services = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const extractServices = (data: Environment | undefined) => {
|
export const extractServices = (data: Environment | undefined) => {
|
||||||
const applications: Services[] =
|
const applications: Services[] = (data?.applications?.map((item) => ({
|
||||||
data?.applications.map((item) => ({
|
appName: item.appName,
|
||||||
appName: item.appName,
|
name: item.name,
|
||||||
name: item.name,
|
type: "application",
|
||||||
type: "application",
|
id: item.applicationId,
|
||||||
id: item.applicationId,
|
createdAt: item.createdAt,
|
||||||
createdAt: item.createdAt,
|
status: item.applicationStatus,
|
||||||
status: item.applicationStatus,
|
description: item.description,
|
||||||
description: item.description,
|
serverId: item.serverId,
|
||||||
serverId: item.serverId,
|
})) ?? []) as Services[];
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const mariadb: Services[] =
|
const mariadb: Services[] =
|
||||||
data?.mariadb.map((item) => ({
|
data?.mariadb.map((item) => ({
|
||||||
@@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => {
|
|||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const compose: Services[] =
|
const compose: Services[] = (data?.compose?.map((item) => ({
|
||||||
data?.compose.map((item) => ({
|
appName: item.appName,
|
||||||
appName: item.appName,
|
name: item.name,
|
||||||
name: item.name,
|
type: "compose",
|
||||||
type: "compose",
|
id: item.composeId,
|
||||||
id: item.composeId,
|
createdAt: item.createdAt,
|
||||||
createdAt: item.createdAt,
|
status: item.composeStatus,
|
||||||
status: item.composeStatus,
|
description: item.description,
|
||||||
description: item.description,
|
serverId: item.serverId,
|
||||||
serverId: item.serverId,
|
})) ?? []) as Services[];
|
||||||
})) || [];
|
|
||||||
|
|
||||||
applications.push(
|
applications.push(
|
||||||
...mysql,
|
...mysql,
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get package_version from registry_package events
|
||||||
|
*/
|
||||||
|
const getPackageVersion = (headers: any, body: any) => {
|
||||||
|
const event = headers["x-github-event"];
|
||||||
|
if (event === "registry_package") {
|
||||||
|
return body.registry_package?.package_version;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
@@ -46,21 +57,60 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
|
||||||
|
|
||||||
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const sourceType = application.sourceType;
|
const sourceType = application.sourceType;
|
||||||
|
|
||||||
if (sourceType === "docker") {
|
if (sourceType === "docker") {
|
||||||
|
const applicationImageName = extractImageName(application.dockerImage);
|
||||||
const applicationDockerTag = extractImageTag(application.dockerImage);
|
const applicationDockerTag = extractImageTag(application.dockerImage);
|
||||||
|
|
||||||
|
const webhookImageName = extractImageNameFromRequest(
|
||||||
|
req.headers,
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
const webhookDockerTag = extractImageTagFromRequest(
|
const webhookDockerTag = extractImageTagFromRequest(
|
||||||
req.headers,
|
req.headers,
|
||||||
req.body,
|
req.body,
|
||||||
);
|
);
|
||||||
if (
|
|
||||||
applicationDockerTag &&
|
if (!applicationImageName) {
|
||||||
webhookDockerTag &&
|
res.status(301).json({
|
||||||
webhookDockerTag !== applicationDockerTag
|
message: "Application Docker Image Name Not Found",
|
||||||
) {
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!webhookImageName) {
|
||||||
|
res.status(301).json({
|
||||||
|
message: "Webhook Docker Image Name Not Found",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate image name matches
|
||||||
|
if (webhookImageName !== applicationImageName) {
|
||||||
|
res.status(301).json({
|
||||||
|
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!applicationDockerTag) {
|
||||||
|
res.status(301).json({
|
||||||
|
message: "Application Docker Tag Not Found",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!webhookDockerTag) {
|
||||||
|
res.status(301).json({
|
||||||
|
message: "Webhook Docker Tag Not Found",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webhookDockerTag !== applicationDockerTag) {
|
||||||
res.status(301).json({
|
res.status(301).json({
|
||||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
||||||
});
|
});
|
||||||
@@ -191,7 +241,7 @@ export default async function handler(
|
|||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: application.applicationId as string,
|
applicationId: application.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }),
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
server: !!application.serverId,
|
server: !!application.serverId,
|
||||||
@@ -222,6 +272,39 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the image name without the tag
|
||||||
|
* Example: "my-image" => "my-image"
|
||||||
|
* Example: "my-image:latest" => "my-image"
|
||||||
|
* Example: "my-image:1.0.0" => "my-image"
|
||||||
|
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "myregistryhost:5000/fedora/httpd"
|
||||||
|
* @link https://docs.docker.com/reference/cli/docker/image/tag/
|
||||||
|
*/
|
||||||
|
export function extractImageName(dockerImage: string | null): string | null {
|
||||||
|
if (!dockerImage || typeof dockerImage !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle case where there's no tag (no colon or colon is part of port number)
|
||||||
|
const lastColonIndex = dockerImage.lastIndexOf(":");
|
||||||
|
if (lastColonIndex === -1) {
|
||||||
|
return dockerImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the part after the last colon looks like a tag (not a port number)
|
||||||
|
// Port numbers are typically 1-5 digits, tags are usually longer or contain letters
|
||||||
|
const afterColon = dockerImage.substring(lastColonIndex + 1);
|
||||||
|
const isPortNumber = /^\d{1,5}$/.test(afterColon);
|
||||||
|
|
||||||
|
// If it's a port number (like registry:5000/image), don't split
|
||||||
|
if (isPortNumber) {
|
||||||
|
return dockerImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, split at the last colon to get image name
|
||||||
|
return dockerImage.substring(0, lastColonIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the last part of the image name, which is the tag
|
* Return the last part of the image name, which is the tag
|
||||||
* Example: "my-image" => null
|
* Example: "my-image" => null
|
||||||
@@ -230,7 +313,7 @@ export default async function handler(
|
|||||||
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
|
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
|
||||||
* @link https://docs.docker.com/reference/cli/docker/image/tag/
|
* @link https://docs.docker.com/reference/cli/docker/image/tag/
|
||||||
*/
|
*/
|
||||||
function extractImageTag(dockerImage: string | null) {
|
export function extractImageTag(dockerImage: string | null) {
|
||||||
if (!dockerImage || typeof dockerImage !== "string") {
|
if (!dockerImage || typeof dockerImage !== "string") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -240,12 +323,78 @@ function extractImageTag(dockerImage: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Extract the image name (without tag) from webhook request
|
||||||
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
|
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
|
||||||
|
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
|
||||||
|
*/
|
||||||
|
export const extractImageNameFromRequest = (
|
||||||
|
headers: any,
|
||||||
|
body: any,
|
||||||
|
): string | null => {
|
||||||
|
// GitHub Packages: registry_package events (container registry)
|
||||||
|
const packageVersion = getPackageVersion(headers, body);
|
||||||
|
if (packageVersion?.package_url) {
|
||||||
|
const packageUrl = packageVersion.package_url;
|
||||||
|
// Remove tag if present (everything after the last colon)
|
||||||
|
if (packageUrl.includes(":")) {
|
||||||
|
const lastColonIndex = packageUrl.lastIndexOf(":");
|
||||||
|
// Check if it's a port number (like registry:5000/image)
|
||||||
|
const afterColon = packageUrl.substring(lastColonIndex + 1);
|
||||||
|
const isPortNumber = /^\d{1,5}$/.test(afterColon);
|
||||||
|
if (isPortNumber) {
|
||||||
|
return packageUrl;
|
||||||
|
}
|
||||||
|
return packageUrl.substring(0, lastColonIndex);
|
||||||
|
}
|
||||||
|
return packageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker Hub
|
||||||
|
if (headers["user-agent"]?.includes("Go-http-client")) {
|
||||||
|
if (body.repository) {
|
||||||
|
const repoName = body.repository.repo_name;
|
||||||
|
return `${repoName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
|
||||||
|
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
|
||||||
*/
|
*/
|
||||||
export const extractImageTagFromRequest = (
|
export const extractImageTagFromRequest = (
|
||||||
headers: any,
|
headers: any,
|
||||||
body: any,
|
body: any,
|
||||||
): string | null => {
|
): string | null => {
|
||||||
|
// GitHub Packages: registry_package events (container registry)
|
||||||
|
const packageVersion = getPackageVersion(headers, body);
|
||||||
|
if (packageVersion) {
|
||||||
|
// Try to get tag from container_metadata first (most reliable)
|
||||||
|
// Only use it if it's not empty and not the same as the version (digest)
|
||||||
|
const tagName = packageVersion.container_metadata?.tag?.name?.trim() || "";
|
||||||
|
if (
|
||||||
|
tagName &&
|
||||||
|
tagName !== packageVersion.version &&
|
||||||
|
!tagName.startsWith("sha256:")
|
||||||
|
) {
|
||||||
|
return tagName;
|
||||||
|
}
|
||||||
|
// Fallback: extract tag from package_url (e.g., "ghcr.io/owner/repo:tag")
|
||||||
|
if (packageVersion.package_url) {
|
||||||
|
const packageUrl = packageVersion.package_url;
|
||||||
|
// Handle case where package_url ends with colon (no tag)
|
||||||
|
if (packageUrl.endsWith(":")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tagMatch = packageUrl.match(/:([^:]+)$/);
|
||||||
|
if (tagMatch?.[1]?.trim()) {
|
||||||
|
return tagMatch[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker Hub
|
||||||
if (headers["user-agent"]?.includes("Go-http-client")) {
|
if (headers["user-agent"]?.includes("Go-http-client")) {
|
||||||
if (body.push_data && body.repository) {
|
if (body.push_data && body.repository) {
|
||||||
return body.push_data.tag;
|
return body.push_data.tag;
|
||||||
@@ -255,6 +404,18 @@ export const extractImageTagFromRequest = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const extractCommitMessage = (headers: any, body: any) => {
|
export const extractCommitMessage = (headers: any, body: any) => {
|
||||||
|
// GitHub Packages: registry_package events (container tags)
|
||||||
|
const githubEvent = headers["x-github-event"];
|
||||||
|
if (githubEvent === "registry_package") {
|
||||||
|
const packageVersion = getPackageVersion(headers, body);
|
||||||
|
if (packageVersion) {
|
||||||
|
if (packageVersion.package_url) {
|
||||||
|
return `Docker GHCR image pushed: ${packageVersion.package_url}`;
|
||||||
|
}
|
||||||
|
return "Docker GHCR image pushed";
|
||||||
|
}
|
||||||
|
// If package_version is missing, fall through to default behavior
|
||||||
|
}
|
||||||
// GitHub
|
// GitHub
|
||||||
if (headers["x-github-event"]) {
|
if (headers["x-github-event"]) {
|
||||||
return body.head_commit ? body.head_commit.message : "NEW COMMIT";
|
return body.head_commit ? body.head_commit.message : "NEW COMMIT";
|
||||||
@@ -283,7 +444,7 @@ export const extractCommitMessage = (headers: any, body: any) => {
|
|||||||
|
|
||||||
if (headers["user-agent"]?.includes("Go-http-client")) {
|
if (headers["user-agent"]?.includes("Go-http-client")) {
|
||||||
if (body.push_data && body.repository) {
|
if (body.push_data && body.repository) {
|
||||||
return `Docker image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
|
return `DockerHub image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user