Skip to content

Commit a31487c

Browse files
committed
feat(deploy): add rollback and failure log analysis
1 parent 2b40dec commit a31487c

5 files changed

Lines changed: 218 additions & 31 deletions

File tree

include/vix/cli/commands/deploy/DeployTypes.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ namespace vix::commands::deploy
110110
*/
111111
bool logsOnFailure{true};
112112

113+
/**
114+
* @brief Whether deploy should attempt rollback on failure.
115+
*/
116+
bool rollback{false};
117+
113118
/**
114119
* @brief Number of journalctl log lines shown on failure.
115120
*/

src/commands/DeployCommand.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,16 @@ namespace vix::commands
124124
<< " production.deploy.health_public\n"
125125
<< " production.deploy.proxy_check\n"
126126
<< " production.deploy.proxy_reload\n"
127-
<< " production.deploy.logs_on_failure\n";
127+
<< " production.deploy.logs_on_failure\n"
128+
<< " production.deploy.log_lines\n"
129+
<< " production.deploy.rollback\n"
130+
<< " production.health.service\n"
131+
<< " production.health.local\n"
132+
<< " production.health.public\n"
133+
<< " production.health.websocket\n"
134+
<< " production.logs.service\n"
135+
<< " production.logs.nginx_access\n"
136+
<< " production.logs.nginx_error\n";
128137

129138
return 0;
130139
}

src/commands/deploy/DeployConfig.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ namespace vix::commands::deploy
223223
if (auto logs = read_bool(deploy, "logs_on_failure"))
224224
cfg.logsOnFailure = *logs;
225225

226+
if (auto rollback = read_bool(deploy, "rollback"))
227+
cfg.rollback = *rollback;
228+
226229
if (auto lines = read_int(deploy, "log_lines"))
227230
cfg.logLines = *lines;
228231

src/commands/deploy/DeployOutput.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ namespace vix::commands::deploy::output
4949
vix::cli::util::kv(out, "Proxy check", enabled_disabled(cfg.proxyCheck));
5050
vix::cli::util::kv(out, "Proxy reload", enabled_disabled(cfg.proxyReload));
5151
vix::cli::util::kv(out, "Logs on failure", enabled_disabled(cfg.logsOnFailure));
52+
vix::cli::util::kv(out, "Rollback", enabled_disabled(cfg.rollback));
5253
vix::cli::util::kv(out, "Dry run", yes_no(options.dryRun));
5354
vix::cli::util::kv(out, "Verbose", yes_no(options.verbose));
5455
}

src/commands/deploy/DeployRunner.cpp

Lines changed: 199 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
#include <cstdlib>
1717
#include <iostream>
1818
#include <string>
19+
#include <array>
20+
#include <cstdio>
21+
#include <memory>
22+
#include <optional>
1923

2024
namespace vix::commands::deploy::runner
2125
{
@@ -49,6 +53,41 @@ namespace vix::commands::deploy::runner
4953
return std::system(cmd.c_str()) == 0;
5054
}
5155

56+
std::optional<std::string> run_capture(const std::string &cmd)
57+
{
58+
std::array<char, 2048> buffer{};
59+
std::string output;
60+
61+
std::unique_ptr<FILE, decltype(&pclose)> pipe(
62+
popen(cmd.c_str(), "r"),
63+
pclose);
64+
65+
if (!pipe)
66+
return std::nullopt;
67+
68+
while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe.get()) != nullptr)
69+
output += buffer.data();
70+
71+
while (!output.empty() &&
72+
(output.back() == '\n' ||
73+
output.back() == '\r' ||
74+
output.back() == ' ' ||
75+
output.back() == '\t'))
76+
{
77+
output.pop_back();
78+
}
79+
80+
if (output.empty())
81+
return std::nullopt;
82+
83+
return output;
84+
}
85+
86+
struct DeployState
87+
{
88+
std::string previousGitRef{};
89+
};
90+
5291
int fail(
5392
const DeployConfig &cfg,
5493
const DeployOptions &options,
@@ -60,16 +99,86 @@ namespace vix::commands::deploy::runner
6099
if (!fixMessage.empty())
61100
output::fix(std::cerr, fixMessage);
62101

63-
if (cfg.logsOnFailure && !cfg.serviceName.empty())
102+
if (cfg.logsOnFailure)
103+
{
104+
const std::string logsCmd =
105+
"vix logs errors --repeated -n " +
106+
std::to_string(cfg.logLines);
107+
108+
output::step(std::cout, "Failure Logs");
109+
output::command(std::cout, logsCmd);
110+
111+
if (!options.dryRun)
112+
(void)std::system(logsCmd.c_str());
113+
}
114+
115+
return 1;
116+
}
117+
118+
bool rollback_deploy(
119+
const DeployConfig &cfg,
120+
const DeployOptions &options,
121+
const DeployState &state)
122+
{
123+
if (!cfg.rollback)
124+
return true;
125+
126+
if (state.previousGitRef.empty())
127+
{
128+
output::warn(std::cerr, "rollback skipped: no previous git revision captured");
129+
return false;
130+
}
131+
132+
output::step(std::cout, "Rollback");
133+
134+
const std::string resetCmd =
135+
"git reset --hard " +
136+
shell_quote(state.previousGitRef);
137+
138+
if (!run_cmd(resetCmd, options))
139+
return false;
140+
141+
if (!run_cmd(cfg.buildCommand, options))
142+
return false;
143+
144+
if (!run_cmd("vix service restart", options))
145+
return false;
146+
147+
if (!run_cmd("vix service status", options))
148+
return false;
149+
150+
output::ok(std::cout, "rollback completed");
151+
return true;
152+
}
153+
154+
int fail_with_rollback(
155+
const DeployConfig &cfg,
156+
const DeployOptions &options,
157+
const DeployState &state,
158+
const std::string &message,
159+
const std::string &fixMessage = {})
160+
{
161+
output::error(std::cerr, message);
162+
163+
if (!fixMessage.empty())
164+
output::fix(std::cerr, fixMessage);
165+
166+
if (cfg.rollback)
167+
{
168+
if (!rollback_deploy(cfg, options, state))
169+
{
170+
output::error(std::cerr, "rollback failed");
171+
output::fix(std::cerr, "inspect the repository and service manually");
172+
}
173+
}
174+
175+
if (cfg.logsOnFailure)
64176
{
65177
const std::string logsCmd =
66-
"sudo journalctl -u " +
67-
shell_quote(cfg.serviceName) +
68-
" -n " +
69-
std::to_string(cfg.logLines) +
70-
" --no-pager";
178+
"vix logs errors --repeated -n " +
179+
std::to_string(cfg.logLines);
71180

72-
output::step(std::cout, "Last Logs");
181+
output::step(std::cout, "Failure Logs");
73182
output::command(std::cout, logsCmd);
74183

75184
if (!options.dryRun)
@@ -124,11 +233,7 @@ namespace vix::commands::deploy::runner
124233

125234
output::step(std::cout, "Restart Service");
126235

127-
const std::string cmd =
128-
"sudo systemctl restart " +
129-
shell_quote(cfg.serviceName);
130-
131-
return run_cmd(cmd, options);
236+
return run_cmd("vix service restart", options);
132237
}
133238

134239
bool verify_service(
@@ -140,23 +245,19 @@ namespace vix::commands::deploy::runner
140245

141246
output::step(std::cout, "Service Status");
142247

143-
const std::string cmd =
144-
"systemctl is-active --quiet " +
145-
shell_quote(cfg.serviceName);
146-
147-
return run_cmd(cmd, options);
248+
return run_cmd("vix service status", options);
148249
}
149250

150-
bool service_health(
251+
bool health_check(
151252
const DeployConfig &cfg,
152253
const DeployOptions &options)
153254
{
154255
if (!cfg.healthLocal && !cfg.healthPublic)
155256
return true;
156257

157-
output::step(std::cout, "Service Health");
258+
output::step(std::cout, "Health Check");
158259

159-
return run_cmd("vix service health", options);
260+
return run_cmd("vix health", options);
160261
}
161262

162263
bool proxy_check(
@@ -190,6 +291,18 @@ namespace vix::commands::deploy::runner
190291
{
191292
output::print_summary(std::cout, cfg, options);
192293

294+
DeployState state;
295+
296+
if (cfg.rollback && !options.dryRun)
297+
{
298+
const auto ref = run_capture("git rev-parse HEAD 2>/dev/null");
299+
300+
if (ref)
301+
state.previousGitRef = *ref;
302+
else
303+
output::warn(std::cerr, "rollback enabled but current git revision could not be captured");
304+
}
305+
193306
if (cfg.serviceName.empty())
194307
{
195308
output::error(std::cerr, "Missing deployment service name.");
@@ -198,46 +311,102 @@ namespace vix::commands::deploy::runner
198311
}
199312

200313
if (!pull_latest(cfg, options))
201-
return fail(cfg, options, "git pull failed", "check repository state and branch");
314+
{
315+
return fail_with_rollback(
316+
cfg,
317+
options,
318+
state,
319+
"git pull failed",
320+
"check repository state and branch");
321+
}
202322

203323
if (cfg.pull)
204324
output::ok(std::cout, "git pull completed");
205325

206326
if (!build_app(cfg, options))
207-
return fail(cfg, options, "build failed", "run the configured build command manually");
327+
{
328+
return fail_with_rollback(
329+
cfg,
330+
options,
331+
state,
332+
"build failed",
333+
"run the configured build command manually");
334+
}
208335

209336
output::ok(std::cout, "build completed");
210337

211338
if (!run_tests(cfg, options))
212-
return fail(cfg, options, "tests failed", "run the configured test command manually");
339+
{
340+
return fail_with_rollback(
341+
cfg,
342+
options,
343+
state,
344+
"tests failed",
345+
"run the configured test command manually");
346+
}
213347

214348
if (cfg.tests)
215349
output::ok(std::cout, "tests completed");
216350

217351
if (!restart_service(cfg, options))
218-
return fail(cfg, options, "service restart failed", "run `vix service restart`");
352+
{
353+
return fail_with_rollback(
354+
cfg,
355+
options,
356+
state,
357+
"service restart failed",
358+
"run `vix service restart`");
359+
}
219360

220361
output::ok(std::cout, "service restarted");
221362

222363
if (!verify_service(cfg, options))
223-
return fail(cfg, options, "service is not active", "run `vix service status`");
364+
{
365+
return fail_with_rollback(
366+
cfg,
367+
options,
368+
state,
369+
"service is not active",
370+
"run `vix service status`");
371+
}
224372

225373
output::ok(std::cout, "service is active");
226374

227-
if (!service_health(cfg, options))
228-
return fail(cfg, options, "service health check failed", "run `vix service health`");
375+
if (!health_check(cfg, options))
376+
{
377+
return fail_with_rollback(
378+
cfg,
379+
options,
380+
state,
381+
"health check failed",
382+
"run `vix health`");
383+
}
229384

230385
if (cfg.healthLocal || cfg.healthPublic)
231-
output::ok(std::cout, "service health checks passed");
386+
output::ok(std::cout, "health checks passed");
232387

233388
if (!proxy_check(cfg, options))
234-
return fail(cfg, options, "proxy check failed", "run `vix proxy nginx check`");
389+
{
390+
return fail_with_rollback(
391+
cfg,
392+
options,
393+
state,
394+
"proxy check failed",
395+
"run `vix proxy nginx check`");
396+
}
235397

236398
if (cfg.proxyCheck || cfg.proxyReload)
237399
output::ok(std::cout, "proxy config is valid");
238400

239401
if (!proxy_reload(cfg, options))
240-
return fail(cfg, options, "proxy reload failed", "run `vix proxy nginx reload`");
402+
{
403+
return fail_with_rollback(
404+
cfg,
405+
options,
406+
state,
407+
"proxy reload failed",
408+
"run `vix proxy nginx reload`");
409+
}
241410

242411
if (cfg.proxyReload)
243412
output::ok(std::cout, "proxy reloaded");

0 commit comments

Comments
 (0)