Skip to content

Commit 662ea75

Browse files
committed
fix: honor triggerType on refresh-token webhook deploys (#3710)
The GitHub App webhook handler (github.ts) already respects the application/compose `triggerType` ("push" vs "tag"), but the refresh-token webhook handlers did not look at it at all: - apps/dokploy/pages/api/deploy/[refreshToken].ts - apps/dokploy/pages/api/deploy/compose/[refreshToken].ts For a GitHub source these handlers only checked watchPaths + branch match. As a result a project configured with "On tag" still deployed on every branch push (the field was ignored), and real tag pushes — ref `refs/tags/x`, which extractBranchName leaves unstripped — failed the branch match and never deployed. Add an exported `extractTagName` helper and gate the `github` branch of both handlers on `triggerType`, mirroring github.ts semantics: - triggerType "tag": deploy only on tag events (skip branch/watchPaths, since tags are not branch-scoped and the UI hides watchPaths for tags), titled "Tag created: <tag>"; ignore non-tag pushes. - triggerType "push" (default): ignore tag events; keep the existing branch + watchPaths checks unchanged. Scope is GitHub only, matching the issue and the original feature (PR #1613). No schema, migration, or UI changes — the triggerType column and GitHub UI selector already exist. Adds unit tests for extractTagName across GitHub/Gitea/GitLab/Bitbucket.
1 parent 439f575 commit 662ea75

3 files changed

Lines changed: 174 additions & 32 deletions

File tree

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
extractImageName,
55
extractImageTag,
66
extractImageTagFromRequest,
7+
extractTagName,
78
} from "@/pages/api/deploy/[refreshToken]";
89

910
describe("GitHub Webhook Skip CI", () => {
@@ -113,6 +114,73 @@ describe("GitHub Webhook Skip CI", () => {
113114
});
114115
});
115116

117+
describe("extractTagName", () => {
118+
it("should extract the tag name from a GitHub tag push", () => {
119+
expect(
120+
extractTagName({ "x-github-event": "push" }, { ref: "refs/tags/v1.0.0" }),
121+
).toBe("v1.0.0");
122+
});
123+
124+
it("should return null for a GitHub branch push", () => {
125+
expect(
126+
extractTagName({ "x-github-event": "push" }, { ref: "refs/heads/main" }),
127+
).toBeNull();
128+
});
129+
130+
it("should extract the tag name from a Gitea tag push", () => {
131+
expect(
132+
extractTagName({ "x-gitea-event": "push" }, { ref: "refs/tags/v2.3.4" }),
133+
).toBe("v2.3.4");
134+
});
135+
136+
it("should extract the tag name from a GitLab tag push", () => {
137+
expect(
138+
extractTagName(
139+
{ "x-gitlab-event": "Tag Push Hook" },
140+
{ ref: "refs/tags/release-1" },
141+
),
142+
).toBe("release-1");
143+
});
144+
145+
it("should return null for a GitLab branch push", () => {
146+
expect(
147+
extractTagName(
148+
{ "x-gitlab-event": "Push Hook" },
149+
{ ref: "refs/heads/develop" },
150+
),
151+
).toBeNull();
152+
});
153+
154+
it("should extract the tag name from a Bitbucket tag push", () => {
155+
expect(
156+
extractTagName(
157+
{ "x-event-key": "repo:push" },
158+
{ push: { changes: [{ new: { type: "tag", name: "v9.9.9" } }] } },
159+
),
160+
).toBe("v9.9.9");
161+
});
162+
163+
it("should return null for a Bitbucket branch push", () => {
164+
expect(
165+
extractTagName(
166+
{ "x-event-key": "repo:push" },
167+
{ push: { changes: [{ new: { type: "branch", name: "main" } }] } },
168+
),
169+
).toBeNull();
170+
});
171+
172+
it("should return null when ref is missing or empty", () => {
173+
expect(extractTagName({ "x-github-event": "push" }, {})).toBeNull();
174+
expect(
175+
extractTagName({ "x-github-event": "push" }, { ref: "" }),
176+
).toBeNull();
177+
});
178+
179+
it("should return null for unknown providers", () => {
180+
expect(extractTagName({}, { ref: "refs/tags/v1.0.0" })).toBeNull();
181+
});
182+
});
183+
116184
describe("GitHub Packages Docker Image Tag Extraction", () => {
117185
it("should extract tag from container_metadata", () => {
118186
const headers = { "x-github-event": "registry_package" };

apps/dokploy/pages/api/deploy/[refreshToken].ts

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default async function handler(
6565
return;
6666
}
6767

68-
const deploymentTitle = extractCommitMessage(req.headers, req.body);
68+
let deploymentTitle = extractCommitMessage(req.headers, req.body);
6969

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

126-
const shouldDeployPaths = shouldDeploy(
127-
application.watchPaths,
128-
normalizedCommits,
129-
);
125+
if (application.triggerType === "tag") {
126+
if (!isTagEvent) {
127+
res.status(301).json({
128+
message: "Trigger type is 'tag'; ignoring non-tag push",
129+
});
130+
return;
131+
}
132+
// Tag event: deploy without branch/watchPaths checks (tags are not
133+
// branch-scoped and the UI hides watchPaths for the tag trigger).
134+
deploymentTitle = `Tag created: ${tagName}`;
135+
} else {
136+
if (isTagEvent) {
137+
res.status(301).json({
138+
message: "Trigger type is 'push'; ignoring tag event",
139+
});
140+
return;
141+
}
130142

131-
if (!shouldDeployPaths) {
132-
res.status(301).json({ message: "Watch Paths Not Match" });
133-
return;
134-
}
143+
const normalizedCommits = req.body?.commits?.flatMap(
144+
(commit: any) => commit.modified,
145+
);
135146

136-
const branchName = extractBranchName(req.headers, req.body);
137-
if (!branchName || branchName !== application.branch) {
138-
res.status(301).json({ message: "Branch Not Match" });
139-
return;
147+
const shouldDeployPaths = shouldDeploy(
148+
application.watchPaths,
149+
normalizedCommits,
150+
);
151+
152+
if (!shouldDeployPaths) {
153+
res.status(301).json({ message: "Watch Paths Not Match" });
154+
return;
155+
}
156+
157+
const branchName = extractBranchName(req.headers, req.body);
158+
if (!branchName || branchName !== application.branch) {
159+
res.status(301).json({ message: "Branch Not Match" });
160+
return;
161+
}
140162
}
141163
} else if (sourceType === "git") {
142164
const branchName = extractBranchName(req.headers, req.body);
@@ -535,6 +557,35 @@ export const extractBranchName = (headers: any, body: any) => {
535557
return null;
536558
};
537559

560+
/**
561+
* Return the tag name for a tag-creation webhook event, or null when the event
562+
* is not a tag push. Used to honor the application/compose `triggerType` ("tag"
563+
* vs "push") on the refresh-token webhook path, mirroring the GitHub App handler.
564+
*/
565+
export const extractTagName = (headers: any, body: any) => {
566+
// GitHub / Gitea: ref = refs/tags/<tag>
567+
if (headers["x-github-event"] || headers["x-gitea-event"]) {
568+
return body?.ref?.startsWith("refs/tags/")
569+
? body.ref.replace("refs/tags/", "")
570+
: null;
571+
}
572+
573+
// GitLab: Tag Push Hook, ref = refs/tags/<tag>
574+
if (headers["x-gitlab-event"]) {
575+
return body?.ref?.startsWith("refs/tags/")
576+
? body.ref.replace("refs/tags/", "")
577+
: null;
578+
}
579+
580+
// Bitbucket: push change with new.type === "tag"
581+
if (headers["x-event-key"]?.includes("repo:push")) {
582+
const change = body?.push?.changes?.[0]?.new;
583+
return change?.type === "tag" ? change?.name : null;
584+
}
585+
586+
return null;
587+
};
588+
538589
export const getProviderByHeader = (headers: any) => {
539590
if (headers["x-github-event"]) {
540591
return "github";

apps/dokploy/pages/api/deploy/compose/[refreshToken].ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
extractCommitMessage,
1212
extractCommittedPaths,
1313
extractHash,
14+
extractTagName,
1415
getProviderByHeader,
1516
logWebhookError,
1617
} from "../[refreshToken]";
@@ -48,29 +49,51 @@ export default async function handler(
4849
return;
4950
}
5051

51-
const deploymentTitle = extractCommitMessage(req.headers, req.body);
52+
let deploymentTitle = extractCommitMessage(req.headers, req.body);
5253
const deploymentHash = extractHash(req.headers, req.body);
5354
const sourceType = composeResult.sourceType;
5455

5556
if (sourceType === "github") {
56-
const branchName = extractBranchName(req.headers, req.body);
57-
const normalizedCommits = req.body?.commits?.flatMap(
58-
(commit: any) => commit.modified,
59-
);
57+
const tagName = extractTagName(req.headers, req.body);
58+
const isTagEvent = !!tagName;
59+
60+
if (composeResult.triggerType === "tag") {
61+
if (!isTagEvent) {
62+
res.status(301).json({
63+
message: "Trigger type is 'tag'; ignoring non-tag push",
64+
});
65+
return;
66+
}
67+
// Tag event: deploy without branch/watchPaths checks (tags are not
68+
// branch-scoped and the UI hides watchPaths for the tag trigger).
69+
deploymentTitle = `Tag created: ${tagName}`;
70+
} else {
71+
if (isTagEvent) {
72+
res.status(301).json({
73+
message: "Trigger type is 'push'; ignoring tag event",
74+
});
75+
return;
76+
}
77+
78+
const branchName = extractBranchName(req.headers, req.body);
79+
const normalizedCommits = req.body?.commits?.flatMap(
80+
(commit: any) => commit.modified,
81+
);
6082

61-
const shouldDeployPaths = shouldDeploy(
62-
composeResult.watchPaths,
63-
normalizedCommits,
64-
);
83+
const shouldDeployPaths = shouldDeploy(
84+
composeResult.watchPaths,
85+
normalizedCommits,
86+
);
6587

66-
if (!shouldDeployPaths) {
67-
res.status(301).json({ message: "Watch Paths Not Match" });
68-
return;
69-
}
88+
if (!shouldDeployPaths) {
89+
res.status(301).json({ message: "Watch Paths Not Match" });
90+
return;
91+
}
7092

71-
if (!branchName || branchName !== composeResult.branch) {
72-
res.status(301).json({ message: "Branch Not Match" });
73-
return;
93+
if (!branchName || branchName !== composeResult.branch) {
94+
res.status(301).json({ message: "Branch Not Match" });
95+
return;
96+
}
7497
}
7598
} else if (sourceType === "gitlab") {
7699
const branchName = extractBranchName(req.headers, req.body);

0 commit comments

Comments
 (0)