Skip to content

Commit 5c4cb2e

Browse files
kporras07claude
andcommitted
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>
1 parent b5a1121 commit 5c4cb2e

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

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

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)