Skip to content

Commit 2a63d4d

Browse files
authored
Ported recent shell script changes to PHP tooling package. (#2472)
1 parent 106ba97 commit 2a63d4d

17 files changed

Lines changed: 407 additions & 21 deletions

.vortex/tooling/src/download-db-acquia

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,25 +224,35 @@ else {
224224
note(sprintf('Using the latest backup ID %s for DB %s.', $backup_id, $db_name));
225225

226226
task('Discovering backup URL.');
227-
$backup_url_response = request_get(sprintf('https://cloud.acquia.com/api/environments/%s/databases/%s/backups/%s/actions/download', $env_id, $db_name, $backup_id), dl_acquia_api_headers($token));
227+
// The Acquia API responds with a 302 redirect to the final S3 download
228+
// URL. Capture the redirect URL without following it to avoid sending the
229+
// Authorization header to S3 - the auth header is required only to query
230+
// the Acquia API.
231+
$backup_url_response = request(sprintf('https://cloud.acquia.com/api/environments/%s/databases/%s/backups/%s/actions/download', $env_id, $db_name, $backup_id), ['method' => 'GET', 'headers' => dl_acquia_api_headers($token), 'follow_redirects' => FALSE]);
228232

229233
if (!$backup_url_response['ok']) {
230-
fail('Failed to retrieve backup URL for backup ID \'%s\'.', $backup_id);
234+
fail(sprintf(
235+
'Unable to discover backup URL for backup ID \'%s\' (status: %s, error: %s).',
236+
$backup_id,
237+
(string) ($backup_url_response['status'] ?? 'unknown'),
238+
(string) ($backup_url_response['error'] ?? 'none')
239+
));
231240
}
232241

233-
$backup_url_data = json_decode((string) $backup_url_response['body'], TRUE);
234-
$backup_url = $backup_url_data['url'] ?? '';
242+
$backup_url = (string) ($backup_url_response['info']['redirect_url'] ?? '');
235243

236244
if ($backup_url === '') {
237245
fail(sprintf('Unable to discover backup URL for backup ID \'%s\'.', $backup_id));
238246
}
239247

240248
task(sprintf('Downloading DB dump into file %s.', $file_name_compressed));
241-
$dl_response = request($backup_url, ['method' => 'GET', 'headers' => dl_acquia_api_headers($token), 'save_to' => $file_name_compressed, 'timeout' => 600]);
249+
$dl_response = request($backup_url, ['method' => 'GET', 'save_to' => $file_name_compressed, 'timeout' => 600]);
242250
if (!$dl_response['ok']) {
243251
fail('Unable to download database %s.', $db_name);
244252
}
245253

254+
// Check if the downloaded file exists and has content. Leave the file in
255+
// place on failure so it can be inspected.
246256
if (!file_exists($file_name_compressed) || filesize($file_name_compressed) === 0) {
247257
fail(sprintf('Downloaded file is empty or missing: %s', $file_name_compressed));
248258
}
@@ -253,15 +263,15 @@ else {
253263

254264
task(sprintf('Expanding DB file %s into %s.', $file_name_compressed, $file_name));
255265

266+
// Test the gzip file first to ensure it's valid. Leave the file in place
267+
// on failure so it can be inspected.
256268
$header = file_get_contents($file_name_compressed, FALSE, NULL, 0, 2);
257269
if ($header === FALSE || $header !== "\x1f\x8b") {
258-
@unlink($file_name_compressed);
259270
fail(sprintf('Downloaded file is not a valid gzip archive: %s', $file_name_compressed));
260271
}
261272

262273
$gz_in = @fopen('compress.zlib://' . $file_name_compressed, 'rb');
263274
if ($gz_in === FALSE) {
264-
@unlink($file_name_compressed);
265275
// @codeCoverageIgnoreStart
266276
fail(sprintf('Unable to read compressed file %s.', $file_name_compressed));
267277
// @codeCoverageIgnoreEnd
@@ -270,7 +280,6 @@ else {
270280
$out = @fopen($file_name, 'wb');
271281
if ($out === FALSE) {
272282
fclose($gz_in);
273-
@unlink($file_name_compressed);
274283
// @codeCoverageIgnoreStart
275284
fail(sprintf('Unable to write decompressed file %s.', $file_name));
276285
// @codeCoverageIgnoreEnd
@@ -280,17 +289,17 @@ else {
280289
fclose($gz_in);
281290
fclose($out);
282291

292+
// Check decompression result and file validity. Leave both files in place
293+
// on failure so they can be inspected.
283294
if ($bytes === FALSE || $bytes === 0) {
284-
@unlink($file_name_compressed);
285-
@unlink($file_name);
286295
fail(sprintf('Downloaded file is not a valid gzip archive: %s', $file_name_compressed));
287296
}
288297

289-
@unlink($file_name_compressed);
290-
291298
if (!file_exists($file_name) || filesize($file_name) === 0) {
292299
fail(sprintf('Unable to process DB dump file "%s".', $file_name));
293300
}
301+
302+
@unlink($file_name_compressed);
294303
}
295304

296305
task(sprintf('Renaming file "%s" to "%s/%s".', $file_name, $db_dir, $db_file));

.vortex/tooling/src/helpers.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,15 +546,17 @@ function request_post(string $url, $body = NULL, array $headers = [], int $timeo
546546
*
547547
* @param string $url
548548
* URL to request.
549-
* @param array{method?: string, headers?: array<int, string>, body?: mixed, timeout?: int, save_to?: string, upload_file?: string, auth?: string} $options
549+
* @param array{method?: string, headers?: array<int, string>, body?: mixed, timeout?: int, save_to?: string, upload_file?: string, auth?: string, follow_redirects?: bool} $options
550550
* Array of options:
551551
* - method: HTTP method (GET, POST, PUT, etc.)
552552
* - headers: Array of HTTP headers
553553
* - body: Request body
554554
* - timeout: Request timeout in seconds
555555
* - save_to: Path to save response body to file
556556
* - upload_file: Path to file to upload (sets CURLOPT_UPLOAD)
557-
* - auth: 'user:pass' for CURLOPT_USERPWD authentication.
557+
* - auth: 'user:pass' for CURLOPT_USERPWD authentication
558+
* - follow_redirects: Whether to follow HTTP redirects (default: TRUE).
559+
* When FALSE, the redirect target is exposed via info.redirect_url.
558560
*
559561
* @return array{ok: bool, status: int, body: string|false, error: string|null, info: array<string, mixed>}
560562
* Array with keys:
@@ -579,7 +581,7 @@ function request(string $url, array $options = []): array {
579581
/** @var array<int, mixed> $opts */
580582
$opts = [
581583
CURLOPT_RETURNTRANSFER => TRUE,
582-
CURLOPT_FOLLOWLOCATION => TRUE,
584+
CURLOPT_FOLLOWLOCATION => $options['follow_redirects'] ?? TRUE,
583585
CURLOPT_TIMEOUT => $options['timeout'] ?? 10,
584586
];
585587

.vortex/tooling/src/notify-email

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,25 @@ namespace DrevOps\VortexTooling;
1313
require_once __DIR__ . '/helpers.php';
1414
execute_override(basename(__FILE__));
1515

16+
// -----------------------------------------------------------------------------
17+
// Branch filter gate.
18+
//
19+
// Comma-separated list of branch names. When set, email notifications are
20+
// only sent for deployments on the listed branches. When empty, no filtering
21+
// is applied. Checked before required variable validation so channels not
22+
// configured for the current branch can short-circuit cleanly.
23+
// -----------------------------------------------------------------------------
24+
25+
$notify_email_branches = getenv_default('VORTEX_NOTIFY_EMAIL_BRANCHES', '');
26+
if ($notify_email_branches !== '') {
27+
$current_branch = (string) (getenv('VORTEX_NOTIFY_BRANCH') ?: '');
28+
$branch_list = array_map('trim', explode(',', $notify_email_branches));
29+
if (!in_array($current_branch, $branch_list, TRUE)) {
30+
pass(sprintf('Skipping email notification for branch \'%s\'.', $current_branch));
31+
quit();
32+
}
33+
}
34+
1635
// -----------------------------------------------------------------------------
1736

1837
// Email notification project name.

.vortex/tooling/src/notify-github

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ namespace DrevOps\VortexTooling;
2020
require_once __DIR__ . '/helpers.php';
2121
execute_override(basename(__FILE__));
2222

23+
// -----------------------------------------------------------------------------
24+
// Branch filter gate.
25+
//
26+
// Comma-separated list of branch names. When set, GitHub notifications are
27+
// only sent for deployments on the listed branches. When empty, no filtering
28+
// is applied. Checked before required variable validation so channels not
29+
// configured for the current branch can short-circuit cleanly.
30+
// -----------------------------------------------------------------------------
31+
32+
$notify_github_branches = getenv_default('VORTEX_NOTIFY_GITHUB_BRANCHES', '');
33+
if ($notify_github_branches !== '') {
34+
$current_branch = (string) (getenv('VORTEX_NOTIFY_BRANCH') ?: '');
35+
$branch_list = array_map('trim', explode(',', $notify_github_branches));
36+
if (!in_array($current_branch, $branch_list, TRUE)) {
37+
pass(sprintf('Skipping GitHub notification for branch \'%s\'.', $current_branch));
38+
quit();
39+
}
40+
}
41+
2342
// -----------------------------------------------------------------------------
2443

2544
// GitHub notification personal access token.

.vortex/tooling/src/notify-jira

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ namespace DrevOps\VortexTooling;
2020
require_once __DIR__ . '/helpers.php';
2121
execute_override(basename(__FILE__));
2222

23+
// -----------------------------------------------------------------------------
24+
// Branch filter gate.
25+
//
26+
// Comma-separated list of branch names. When set, JIRA notifications are
27+
// only sent for deployments on the listed branches. When empty, no filtering
28+
// is applied. Checked before required variable validation so channels not
29+
// configured for the current branch can short-circuit cleanly.
30+
// -----------------------------------------------------------------------------
31+
32+
$notify_jira_branches = getenv_default('VORTEX_NOTIFY_JIRA_BRANCHES', '');
33+
if ($notify_jira_branches !== '') {
34+
$current_branch = (string) (getenv('VORTEX_NOTIFY_BRANCH') ?: '');
35+
$branch_list = array_map('trim', explode(',', $notify_jira_branches));
36+
if (!in_array($current_branch, $branch_list, TRUE)) {
37+
pass(sprintf('Skipping JIRA notification for branch \'%s\'.', $current_branch));
38+
quit();
39+
}
40+
}
41+
2342
// -----------------------------------------------------------------------------
2443

2544
// JIRA notification project name.

.vortex/tooling/src/notify-newrelic

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ if (empty($newrelic_enabled)) {
9292
quit();
9393
}
9494

95+
// Branch filter gate.
96+
//
97+
// Comma-separated list of branch names. When set, New Relic notifications
98+
// are only sent for deployments on the listed branches. When empty, no
99+
// filtering is applied. Defaults to the long-lived branches.
100+
$notify_newrelic_branches = getenv_default('VORTEX_NOTIFY_NEWRELIC_BRANCHES', 'main,master,develop');
101+
if ($notify_newrelic_branches !== '') {
102+
$current_branch = (string) (getenv('VORTEX_NOTIFY_BRANCH') ?: '');
103+
$branch_list = array_map('trim', explode(',', $notify_newrelic_branches));
104+
if (!in_array($current_branch, $branch_list, TRUE)) {
105+
pass(sprintf('Skipping New Relic notification for branch \'%s\'.', $current_branch));
106+
quit();
107+
}
108+
}
109+
95110
info('Started New Relic notification.');
96111

97112
// Skip if this is a pre-deployment event (New Relic only for post-deployment).

.vortex/tooling/src/notify-slack

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ namespace DrevOps\VortexTooling;
1515
require_once __DIR__ . '/helpers.php';
1616
execute_override(basename(__FILE__));
1717

18+
// -----------------------------------------------------------------------------
19+
// Branch filter gate.
20+
//
21+
// Comma-separated list of branch names. When set, Slack notifications are
22+
// only sent for deployments on the listed branches. When empty, no filtering
23+
// is applied. Checked before required variable validation so channels not
24+
// configured for the current branch can short-circuit cleanly.
25+
// -----------------------------------------------------------------------------
26+
27+
$notify_slack_branches = getenv_default('VORTEX_NOTIFY_SLACK_BRANCHES', '');
28+
if ($notify_slack_branches !== '') {
29+
$current_branch = (string) (getenv('VORTEX_NOTIFY_BRANCH') ?: '');
30+
$branch_list = array_map('trim', explode(',', $notify_slack_branches));
31+
if (!in_array($current_branch, $branch_list, TRUE)) {
32+
pass(sprintf('Skipping Slack notification for branch \'%s\'.', $current_branch));
33+
quit();
34+
}
35+
}
36+
1837
// -----------------------------------------------------------------------------
1938

2039
// Slack notification project name.

.vortex/tooling/src/notify-webhook

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,25 @@ namespace DrevOps\VortexTooling;
1313
require_once __DIR__ . '/helpers.php';
1414
execute_override(basename(__FILE__));
1515

16+
// -----------------------------------------------------------------------------
17+
// Branch filter gate.
18+
//
19+
// Comma-separated list of branch names. When set, Webhook notifications are
20+
// only sent for deployments on the listed branches. When empty, no filtering
21+
// is applied. Checked before required variable validation so channels not
22+
// configured for the current branch can short-circuit cleanly.
23+
// -----------------------------------------------------------------------------
24+
25+
$notify_webhook_branches = getenv_default('VORTEX_NOTIFY_WEBHOOK_BRANCHES', '');
26+
if ($notify_webhook_branches !== '') {
27+
$current_branch = (string) (getenv('VORTEX_NOTIFY_BRANCH') ?: '');
28+
$branch_list = array_map('trim', explode(',', $notify_webhook_branches));
29+
if (!in_array($current_branch, $branch_list, TRUE)) {
30+
pass(sprintf('Skipping Webhook notification for branch \'%s\'.', $current_branch));
31+
quit();
32+
}
33+
}
34+
1635
// -----------------------------------------------------------------------------
1736

1837
// Webhook notification project name.

.vortex/tooling/src/provision

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ $provision_verify_config = getenv_default('VORTEX_PROVISION_VERIFY_CONFIG_UNCHAN
5656
// Skip cache rebuild after database updates.
5757
$provision_cache_rebuild_after_db_update_skip = getenv_default('VORTEX_PROVISION_CACHE_REBUILD_AFTER_DB_UPDATE_SKIP', '0');
5858

59+
// Repeat configuration import after the initial import.
60+
//
61+
// Useful when update hooks introduce new configuration that affects
62+
// subsequent configuration imports (e.g., new config_split settings).
63+
$provision_config_import_repeat = getenv_default('VORTEX_PROVISION_CONFIG_IMPORT_REPEAT', '0');
64+
5965
// Provision database dump file.
6066
//
6167
// If not set, it will be auto-discovered from the VORTEX_PROVISION_DB_DIR
@@ -548,6 +554,13 @@ if ($site_has_config_files) {
548554
pass('Completed configuration import.');
549555
echo PHP_EOL;
550556

557+
if ($provision_config_import_repeat === '1') {
558+
task('Repeating configuration import.');
559+
drush('config:import');
560+
pass('Completed repeated configuration import.');
561+
echo PHP_EOL;
562+
}
563+
551564
// Import config_split configuration if the module is installed.
552565
// Drush deploy does not import config_split configuration on the first run.
553566
// @see https://github.com/drush-ops/drush/issues/2449

.vortex/tooling/tests/Unit/DownloadDbAcquiaTest.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ public function testDownloadRequestFails(): void {
156156
'url' => 'https://cloud.acquia.com/api/environments/env-id-prod/databases/mydb/backups?sort=created',
157157
'response' => ['body' => json_encode(['_embedded' => ['items' => [['id' => '12345']]]])],
158158
],
159-
// Backup download URL.
159+
// Backup download URL: API responds with 302 redirect to S3.
160160
[
161161
'url' => 'https://cloud.acquia.com/api/environments/env-id-prod/databases/mydb/backups/12345/actions/download',
162-
'response' => ['body' => json_encode(['url' => 'https://acquia-backup.s3.amazonaws.com/backup.sql.gz'])],
162+
'response' => ['status' => 302, 'info' => ['redirect_url' => 'https://acquia-backup.s3.amazonaws.com/backup.sql.gz']],
163163
],
164164
// Download request fails.
165165
[
@@ -177,7 +177,8 @@ public function testInvalidGzip(): void {
177177
mkdir($db_dir, 0755, TRUE);
178178

179179
// Pre-create an invalid gz file.
180-
file_put_contents($db_dir . '/mydb_backup_12345.sql.gz', 'NOT VALID GZIP DATA');
180+
$invalid_gz = $db_dir . '/mydb_backup_12345.sql.gz';
181+
file_put_contents($invalid_gz, 'NOT VALID GZIP DATA');
181182

182183
$this->mockRequestMultiple([
183184
[
@@ -199,6 +200,9 @@ public function testInvalidGzip(): void {
199200
]);
200201

201202
$this->runScriptError('src/download-db-acquia', 'Downloaded file is not a valid gzip archive');
203+
204+
// Invalid file is left in place for inspection.
205+
$this->assertFileExists($invalid_gz);
202206
}
203207

204208
public function testNoBackups(): void {
@@ -246,10 +250,10 @@ public function testBackupUrlEmpty(): void {
246250
'url' => 'https://cloud.acquia.com/api/environments/env-id-prod/databases/mydb/backups?sort=created',
247251
'response' => ['body' => json_encode(['_embedded' => ['items' => [['id' => '12345']]]])],
248252
],
249-
// Backup download URL returns empty.
253+
// Backup download URL returns no redirect (empty redirect_url).
250254
[
251255
'url' => 'https://cloud.acquia.com/api/environments/env-id-prod/databases/mydb/backups/12345/actions/download',
252-
'response' => ['body' => json_encode([])],
256+
'response' => ['info' => ['redirect_url' => '']],
253257
],
254258
]);
255259

0 commit comments

Comments
 (0)