Skip to content

Commit 570c0fe

Browse files
kporras07claude
andauthored
feat(DEVX-6540): add node:builds:rollback command (#2803)
* feat(DEVX-6540): add node:builds:rollback command Add terminus command to roll back a deployed Node.js build. Calls the pantheonapi rollback endpoint, validates EVCS/Node.js site, and handles error responses from the API. Usage: terminus node:builds:rollback <site>.<env> <build-id> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(DEVX-6540): register BuildsRollbackCommand in Terminus command list The command class existed but wasn't discoverable because it was missing from the explicit command registration in Terminus.php. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(DEVX-6540): check response status code in rollback command Guzzle http_errors is globally disabled in Terminus (constants.yml), so exceptions are never thrown on 4xx/5xx. Check the status code from RequestOperationResult instead and throw TerminusException on failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 13c898d commit 570c0fe

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace Pantheon\Terminus\Commands\Node;
4+
5+
use Pantheon\Terminus\Commands\TerminusCommand;
6+
use Pantheon\Terminus\Exceptions\TerminusException;
7+
use Pantheon\Terminus\Site\SiteAwareInterface;
8+
use Pantheon\Terminus\Site\SiteAwareTrait;
9+
use Pantheon\Terminus\Request\RequestAwareInterface;
10+
use Pantheon\Terminus\Request\RequestAwareTrait;
11+
12+
/**
13+
* Class BuildsRollbackCommand.
14+
*
15+
* @package Pantheon\Terminus\Commands\Node
16+
*/
17+
class BuildsRollbackCommand extends TerminusCommand implements SiteAwareInterface, RequestAwareInterface
18+
{
19+
use SiteAwareTrait;
20+
use RequestAwareTrait;
21+
22+
/**
23+
* Rolls back a deployed build on a Node.js site environment.
24+
*
25+
* @authorize
26+
*
27+
* @command node:builds:rollback
28+
* @aliases nbr,node:build:rollback
29+
*
30+
* @param string $site_env Site & environment in the format `site-name.env`
31+
* @param string $build_id The build ID to roll back to
32+
*
33+
* @throws \Pantheon\Terminus\Exceptions\TerminusException
34+
*
35+
* @usage <site>.<env> <build-id> Roll back <site>'s <env> environment to the specified build.
36+
*/
37+
public function rollback($site_env, $build_id)
38+
{
39+
$this->requireSiteIsNotFrozen($site_env);
40+
$site = $this->getSiteById($site_env);
41+
$env = $this->getEnv($site_env);
42+
43+
if (!$site->isEvcs()) {
44+
throw new TerminusException(
45+
'This command only works for sites using external VCS (GitHub, GitLab, Bitbucket).'
46+
);
47+
}
48+
49+
if (!$site->isNodejs()) {
50+
throw new TerminusException(
51+
'This command only works for Node.js sites.'
52+
);
53+
}
54+
55+
$env_name = $env->getName();
56+
57+
$this->log()->notice(
58+
'Initiating rollback of build {build_id} on {site} environment {env}...',
59+
[
60+
'build_id' => $build_id,
61+
'site' => $site->getName(),
62+
'env' => $env_name,
63+
]
64+
);
65+
66+
$url = sprintf(
67+
'/api/sites/%s/environment/%s/build/%s/rollback',
68+
$site->get('id'),
69+
$env_name,
70+
$build_id
71+
);
72+
73+
$this->postToUrl($url);
74+
75+
$this->log()->notice(
76+
'Rollback successfully initiated for {site} environment {env} (build {build_id}).',
77+
[
78+
'site' => $site->getName(),
79+
'env' => $env_name,
80+
'build_id' => $build_id,
81+
]
82+
);
83+
}
84+
85+
/**
86+
* Send a POST request to a given API path.
87+
*
88+
* @param string $path API path (without protocol/host).
89+
*
90+
* @throws \Pantheon\Terminus\Exceptions\TerminusException
91+
*/
92+
private function postToUrl(string $path): void
93+
{
94+
$protocol = $this->getConfig()->get('protocol');
95+
$host = $this->getConfig()->get('host');
96+
97+
$url = sprintf('%s://%s%s', $protocol, $host, $path);
98+
99+
$options = [
100+
'method' => 'POST',
101+
'headers' => [
102+
'X-Pantheon-Session' => $this->request->session()->get('session'),
103+
],
104+
];
105+
106+
$result = $this->request()->request($url, $options);
107+
$statusCode = $result->getStatusCode();
108+
109+
if ($statusCode < 200 || $statusCode >= 300) {
110+
$data = $result->getData();
111+
$message = 'Rollback request failed.';
112+
if (is_object($data) && !empty($data->error)) {
113+
$message = $data->error;
114+
} elseif (is_string($data) && !empty(trim($data))) {
115+
$message = trim($data);
116+
}
117+
throw new TerminusException($message);
118+
}
119+
}
120+
}

src/Terminus.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ private function addBuiltInCommandsAndHooks()
415415
'Pantheon\\Terminus\\Commands\\NodeLogs\\NodeLogsRuntimeGetCommand',
416416
'Pantheon\\Terminus\\Commands\\Node\\BuildsListCommand',
417417
'Pantheon\\Terminus\\Commands\\Node\\BuildsRebuildCommand',
418+
'Pantheon\\Terminus\\Commands\\Node\\BuildsRollbackCommand',
418419
'Pantheon\\Terminus\\Commands\\Node\\BuildsWaitCommand',
419420
'Pantheon\\Terminus\\Commands\\Org\\InfoCommand',
420421
'Pantheon\\Terminus\\Commands\\Org\\ListCommand',

tests/Functional/RepositoryCommandsTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public function testRepositoryCommandsExist()
1919
{
2020
$this->assertCommandExists('node:builds:list');
2121
$this->assertCommandExists('node:builds:rebuild');
22+
$this->assertCommandExists('node:builds:rollback');
2223
$this->assertCommandExists('vcs:connection:add');
2324
$this->assertCommandExists('vcs:connection:link');
2425
$this->assertCommandExists('vcs:connection:list');

0 commit comments

Comments
 (0)