Deploy Dokploy applications from GitHub and Gitea Actions
A dependency-free node20 action that triggers application.deploy or compose.deploy, then follows the deployment to a terminal status so a failed build fails the CI run. Snapshots the deployment list before triggering, so a concurrent deployment is never mistaken for this one. Optionally points the application at a freshly built image first, which is what lets a workflow lint, test, push and deploy the same artifact.
This commit is contained in:
138
test/client.test.js
Normal file
138
test/client.test.js
Normal file
@@ -0,0 +1,138 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { DokployClient, DokployError, normaliseHost, parseError } = require("../src/client");
|
||||
|
||||
function response(status, body) {
|
||||
return {
|
||||
ok: status < 400,
|
||||
status,
|
||||
text: async () => (typeof body === "string" ? body : JSON.stringify(body)),
|
||||
};
|
||||
}
|
||||
|
||||
function clientWith(fetchImpl, options = {}) {
|
||||
return new DokployClient({
|
||||
host: "https://dokploy.example.com",
|
||||
token: "secret",
|
||||
fetch: fetchImpl,
|
||||
sleep: async () => {},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
test("normaliseHost tolerates trailing slashes and a trailing /api", () => {
|
||||
const expected = "https://dokploy.example.com/api";
|
||||
assert.equal(normaliseHost("https://dokploy.example.com"), expected);
|
||||
assert.equal(normaliseHost("https://dokploy.example.com/"), expected);
|
||||
assert.equal(normaliseHost("https://dokploy.example.com/api"), expected);
|
||||
assert.equal(normaliseHost(" https://dokploy.example.com/api/ "), expected);
|
||||
});
|
||||
|
||||
test("normaliseHost rejects empty and non-HTTP hosts", () => {
|
||||
assert.throws(() => normaliseHost(""), /must not be empty/);
|
||||
assert.throws(() => normaliseHost("dokploy.example.com"), /http:\/\/ or https:\/\//);
|
||||
assert.throws(() => normaliseHost("ftp://dokploy.example.com"), /http:\/\/ or https:\/\//);
|
||||
});
|
||||
|
||||
test("query flattens input into query parameters and drops nulls", async () => {
|
||||
let seen;
|
||||
const api = clientWith(async (url, init) => {
|
||||
seen = { url, init };
|
||||
return response(200, { ok: true });
|
||||
});
|
||||
|
||||
await api.query("deployment.all", { applicationId: "app-1", tail: 100, since: null });
|
||||
|
||||
assert.equal(seen.url, "https://dokploy.example.com/api/deployment.all?applicationId=app-1&tail=100");
|
||||
assert.equal(seen.init.method, "GET");
|
||||
assert.equal(seen.init.headers["x-api-key"], "secret");
|
||||
assert.equal(seen.init.body, undefined);
|
||||
});
|
||||
|
||||
test("mutate posts a JSON body", async () => {
|
||||
let seen;
|
||||
const api = clientWith(async (url, init) => {
|
||||
seen = { url, init };
|
||||
return response(200, true);
|
||||
});
|
||||
|
||||
const result = await api.mutate("application.deploy", { applicationId: "app-1", title: "CI abc1234" });
|
||||
|
||||
assert.equal(seen.url, "https://dokploy.example.com/api/application.deploy");
|
||||
assert.equal(seen.init.method, "POST");
|
||||
assert.equal(seen.init.headers["content-type"], "application/json");
|
||||
assert.deepEqual(JSON.parse(seen.init.body), { applicationId: "app-1", title: "CI abc1234" });
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
test("an error response becomes a DokployError carrying the Zod field errors", async () => {
|
||||
const body = {
|
||||
message: "Invalid input",
|
||||
code: "BAD_REQUEST",
|
||||
data: { code: "BAD_REQUEST", path: "application.deploy", zodError: { fieldErrors: { applicationId: ["Required"] } } },
|
||||
};
|
||||
const api = clientWith(async () => response(400, body));
|
||||
|
||||
await assert.rejects(
|
||||
() => api.mutate("application.deploy", {}),
|
||||
(err) => {
|
||||
assert.ok(err instanceof DokployError);
|
||||
assert.equal(err.status, 400);
|
||||
assert.match(err.message, /HTTP 400/);
|
||||
assert.match(err.message, /BAD_REQUEST/);
|
||||
assert.match(err.message, /applicationId: Required/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("a 500 whose message says 'not found' still counts as not found", () => {
|
||||
const err = parseError(500, "application.one", JSON.stringify({ message: "Application not found" }));
|
||||
assert.equal(err.isNotFound, true);
|
||||
});
|
||||
|
||||
test("queries retry a 5xx, mutations do not", async () => {
|
||||
let queryAttempts = 0;
|
||||
const flaky = clientWith(async () => {
|
||||
queryAttempts += 1;
|
||||
return queryAttempts < 3 ? response(502, "bad gateway") : response(200, { fine: true });
|
||||
});
|
||||
assert.deepEqual(await flaky.query("project.all"), { fine: true });
|
||||
assert.equal(queryAttempts, 3);
|
||||
|
||||
let mutateAttempts = 0;
|
||||
const failing = clientWith(async () => {
|
||||
mutateAttempts += 1;
|
||||
return response(502, "bad gateway");
|
||||
});
|
||||
await assert.rejects(() => failing.mutate("application.deploy", {}));
|
||||
assert.equal(mutateAttempts, 1, "a retried deploy would queue a second build");
|
||||
});
|
||||
|
||||
test("queries give up after the configured number of retries", async () => {
|
||||
let attempts = 0;
|
||||
const api = clientWith(
|
||||
async () => {
|
||||
attempts += 1;
|
||||
return response(500, "boom");
|
||||
},
|
||||
{ retries: 1 },
|
||||
);
|
||||
await assert.rejects(() => api.query("project.all"));
|
||||
assert.equal(attempts, 2);
|
||||
});
|
||||
|
||||
test("an HTML response explains that the host is probably wrong", async () => {
|
||||
const api = clientWith(async () => response(200, "<!doctype html><html><body>nginx</body></html>"));
|
||||
await assert.rejects(() => api.query("project.all"), /is `host` pointing at a Dokploy instance\?/);
|
||||
});
|
||||
|
||||
test("a network failure is reported against the procedure that caused it", async () => {
|
||||
const api = clientWith(async () => {
|
||||
throw new Error("ECONNREFUSED");
|
||||
});
|
||||
await assert.rejects(() => api.mutate("application.deploy", {}), /Calling application\.deploy: ECONNREFUSED/);
|
||||
});
|
||||
Reference in New Issue
Block a user