Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions apps/dokploy/__test__/deploy/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
extractImageName,
extractImageTag,
extractImageTagFromRequest,
extractTagName,
} from "@/pages/api/deploy/[refreshToken]";

describe("GitHub Webhook Skip CI", () => {
Expand Down Expand Up @@ -113,6 +114,73 @@ describe("GitHub Webhook Skip CI", () => {
});
});

describe("extractTagName", () => {
it("should extract the tag name from a GitHub tag push", () => {
expect(
extractTagName({ "x-github-event": "push" }, { ref: "refs/tags/v1.0.0" }),
).toBe("v1.0.0");
});

it("should return null for a GitHub branch push", () => {
expect(
extractTagName({ "x-github-event": "push" }, { ref: "refs/heads/main" }),
).toBeNull();
});

it("should extract the tag name from a Gitea tag push", () => {
expect(
extractTagName({ "x-gitea-event": "push" }, { ref: "refs/tags/v2.3.4" }),
).toBe("v2.3.4");
});

it("should extract the tag name from a GitLab tag push", () => {
expect(
extractTagName(
{ "x-gitlab-event": "Tag Push Hook" },
{ ref: "refs/tags/release-1" },
),
).toBe("release-1");
});

it("should return null for a GitLab branch push", () => {
expect(
extractTagName(
{ "x-gitlab-event": "Push Hook" },
{ ref: "refs/heads/develop" },
),
).toBeNull();
});

it("should extract the tag name from a Bitbucket tag push", () => {
expect(
extractTagName(
{ "x-event-key": "repo:push" },
{ push: { changes: [{ new: { type: "tag", name: "v9.9.9" } }] } },
),
).toBe("v9.9.9");
});

it("should return null for a Bitbucket branch push", () => {
expect(
extractTagName(
{ "x-event-key": "repo:push" },
{ push: { changes: [{ new: { type: "branch", name: "main" } }] } },
),
).toBeNull();
});

it("should return null when ref is missing or empty", () => {
expect(extractTagName({ "x-github-event": "push" }, {})).toBeNull();
expect(
extractTagName({ "x-github-event": "push" }, { ref: "" }),
).toBeNull();
});

it("should return null for unknown providers", () => {
expect(extractTagName({}, { ref: "refs/tags/v1.0.0" })).toBeNull();
});
});

describe("GitHub Packages Docker Image Tag Extraction", () => {
it("should extract tag from container_metadata", () => {
const headers = { "x-github-event": "registry_package" };
Expand Down
83 changes: 67 additions & 16 deletions apps/dokploy/pages/api/deploy/[refreshToken].ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default async function handler(
return;
}

const deploymentTitle = extractCommitMessage(req.headers, req.body);
let deploymentTitle = extractCommitMessage(req.headers, req.body);

const deploymentHash = extractHash(req.headers, req.body);
const sourceType = application.sourceType;
Expand Down Expand Up @@ -119,24 +119,46 @@ export default async function handler(
}
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
} else if (sourceType === "github") {
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const tagName = extractTagName(req.headers, req.body);
const isTagEvent = !!tagName;

const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (application.triggerType === "tag") {
if (!isTagEvent) {
res.status(301).json({
message: "Trigger type is 'tag'; ignoring non-tag push",
});
return;
}
// Tag event: deploy without branch/watchPaths checks (tags are not
// branch-scoped and the UI hides watchPaths for the tag trigger).
deploymentTitle = `Tag created: ${tagName}`;
} else {
if (isTagEvent) {
res.status(301).json({
message: "Trigger type is 'push'; ignoring tag event",
});
return;
}

if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);

const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.branch) {
res.status(301).json({ message: "Branch Not Match" });
return;
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);

if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}

const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.branch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
Expand Down Expand Up @@ -535,6 +557,35 @@ export const extractBranchName = (headers: any, body: any) => {
return null;
};

/**
* Return the tag name for a tag-creation webhook event, or null when the event
* is not a tag push. Used to honor the application/compose `triggerType` ("tag"
* vs "push") on the refresh-token webhook path, mirroring the GitHub App handler.
*/
export const extractTagName = (headers: any, body: any) => {
// GitHub / Gitea: ref = refs/tags/<tag>
if (headers["x-github-event"] || headers["x-gitea-event"]) {
return body?.ref?.startsWith("refs/tags/")
? body.ref.replace("refs/tags/", "")
: null;
}

// GitLab: Tag Push Hook, ref = refs/tags/<tag>
if (headers["x-gitlab-event"]) {
return body?.ref?.startsWith("refs/tags/")
? body.ref.replace("refs/tags/", "")
: null;
}

// Bitbucket: push change with new.type === "tag"
if (headers["x-event-key"]?.includes("repo:push")) {
const change = body?.push?.changes?.[0]?.new;
return change?.type === "tag" ? change?.name : null;
}

return null;
};

export const getProviderByHeader = (headers: any) => {
if (headers["x-github-event"]) {
return "github";
Expand Down
55 changes: 39 additions & 16 deletions apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
extractCommitMessage,
extractCommittedPaths,
extractHash,
extractTagName,
getProviderByHeader,
logWebhookError,
} from "../[refreshToken]";
Expand Down Expand Up @@ -48,29 +49,51 @@ export default async function handler(
return;
}

const deploymentTitle = extractCommitMessage(req.headers, req.body);
let deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const sourceType = composeResult.sourceType;

if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const tagName = extractTagName(req.headers, req.body);
const isTagEvent = !!tagName;

if (composeResult.triggerType === "tag") {
if (!isTagEvent) {
res.status(301).json({
message: "Trigger type is 'tag'; ignoring non-tag push",
});
return;
}
// Tag event: deploy without branch/watchPaths checks (tags are not
// branch-scoped and the UI hides watchPaths for the tag trigger).
deploymentTitle = `Tag created: ${tagName}`;
} else {
if (isTagEvent) {
res.status(301).json({
message: "Trigger type is 'push'; ignoring tag event",
});
return;
}

const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);

const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);

if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}

if (!branchName || branchName !== composeResult.branch) {
res.status(301).json({ message: "Branch Not Match" });
return;
if (!branchName || branchName !== composeResult.branch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
Expand Down