Skip to content

Commit 0b4f47e

Browse files
committed
Merge branch 'main' into fix-add-sample-to-readme
2 parents 14c8efe + 95accf0 commit 0b4f47e

9 files changed

Lines changed: 277 additions & 91 deletions

File tree

dev/google-cloud

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require __DIR__ . '/vendor/autoload.php';
2525
use Google\Cloud\Dev\Command\ComponentInfoCommand;
2626
use Google\Cloud\Dev\Command\DocFxCommand;
2727
use Google\Cloud\Dev\Command\NewComponentCommand;
28+
use Google\Cloud\Dev\Command\AddVersionCommand;
2829
use Google\Cloud\Dev\Command\RepoInfoCommand;
2930
use Google\Cloud\Dev\Command\ReleaseInfoCommand;
3031
use Google\Cloud\Dev\Command\SplitCommand;
@@ -47,6 +48,7 @@ $app = new Application;
4748
$app->add(new ComponentInfoCommand());
4849
$app->add(new DocFxCommand());
4950
$app->add(new NewComponentCommand($rootDirectory));
51+
$app->add(new AddVersionCommand($rootDirectory));
5052
$app->add(new RepoInfoCommand());
5153
$app->add(new ReleaseInfoCommand());
5254
$app->add(new SplitCommand($rootDirectory));
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
/**
3+
* Copyright 2023 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Google\Cloud\Dev\Command;
19+
20+
use Symfony\Component\Console\Command\Command;
21+
use Symfony\Component\Console\Input\ArrayInput;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Input\InputArgument;
24+
use Symfony\Component\Console\Input\InputOption;
25+
use Symfony\Component\Console\Output\OutputInterface;
26+
use Symfony\Component\Yaml\Yaml;
27+
use RuntimeException;
28+
use Google\Cloud\Dev\Component;
29+
30+
/**
31+
* Add a Component
32+
* @internal
33+
*/
34+
class AddVersionCommand extends Command
35+
{
36+
private const OWL_BOT_REGEX='/.*\/\(([\w|]+)\).*/';
37+
38+
protected function configure()
39+
{
40+
$this->setName('add-version')
41+
->setDescription('Add a new version to an existing Component')
42+
->addArgument('component', InputArgument::REQUIRED, 'Component to add the version to.')
43+
->addArgument('version', InputArgument::REQUIRED, 'The new version to add.')
44+
->addOption(
45+
'no-update-component',
46+
null,
47+
InputOption::VALUE_NONE,
48+
'Do not run the update-component command after adding the component skeleton'
49+
)
50+
->addOption(
51+
'timeout',
52+
null,
53+
InputOption::VALUE_REQUIRED,
54+
'The timeout limit for executing commands in seconds. Defaults to 60.',
55+
120
56+
);
57+
}
58+
59+
private $rootPath;
60+
61+
/**
62+
* @param string $rootPath The path to the repository root directory.\
63+
*/
64+
public function __construct($rootPath)
65+
{
66+
$this->rootPath = realpath($rootPath);
67+
parent::__construct();
68+
}
69+
70+
protected function execute(InputInterface $input, OutputInterface $output)
71+
{
72+
$componentName = $input->getArgument('component');
73+
$version = $input->getArgument('version');
74+
75+
// Ensure component exists
76+
$owlbotFile = sprintf('%s/%s/.OwlBot.yaml', $this->rootPath, $componentName);
77+
if (!file_exists($owlbotFile)) {
78+
throw new RuntimeException("Component '$componentName' not found.");
79+
}
80+
$output->writeln("Adding new version '$version' to .OwlBot.yaml.");
81+
$yaml = Yaml::parse(file_get_contents($owlbotFile));
82+
foreach ($yaml['deep-copy-regex'] as $i => $deepCopyRegex) {
83+
if (preg_match(self::OWL_BOT_REGEX, $deepCopyRegex['source'], $matches)) {
84+
// ensure version doesn't already exist in .OwlBot.yaml before adding it
85+
if (false !== array_search($version, explode('|', $matches[1]))) {
86+
$output->writeln("Version '$version' already exists in deep-copy-regex. Skipping... ");
87+
continue;
88+
}
89+
$newVersion = $matches[1] . '|' . $version;
90+
$yaml['deep-copy-regex'][$i]['source'] = str_replace($matches[1], $newVersion, $matches[0]);
91+
}
92+
}
93+
// Ensure YAML has changed before writing it
94+
if ($yaml != Yaml::parse(file_get_contents($owlbotFile))) {
95+
file_put_contents($owlbotFile, Yaml::dump($yaml, 3, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
96+
}
97+
98+
// Run "update-component" command to generate the new version and add its sample to the README
99+
if ($input->getOption('no-update-component')) {
100+
// nothing left to do
101+
$output->writeln('Skipping component update: "--no-update-component" flag set');
102+
return 0;
103+
}
104+
105+
$args = [
106+
'component' => $componentName,
107+
'--timeout' => $input->getOption('timeout'),
108+
];
109+
if (!$this->getApplication()->has('update-component')) {
110+
throw new \RuntimeException(
111+
'Application does not have an update-component command. '
112+
. 'Run with --no-update-component to skip this.'
113+
);
114+
}
115+
$updateCommand = $this->getApplication()->find('update-component');
116+
$returnCode = $updateCommand->run(new ArrayInput($args), $output);
117+
if ($returnCode !== Command::SUCCESS) {
118+
return $returnCode;
119+
}
120+
// Run "add-sample-to-readme" command to ensure our README contains the latest version's sample.
121+
$addSamplesArgs = ['--component' => [$componentName], '--update' => true];
122+
if (!$addSampleCommand = $this->getApplication()->find('add-sample-to-readme')) {
123+
throw new \RuntimeException('Application does not have an add-samples-to-readme command.');
124+
}
125+
return $addSampleCommand->run(new ArrayInput($addSamplesArgs), $output);
126+
}
127+
}

dev/src/Command/NewComponentCommand.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Twig\Environment;
3737
use RuntimeException;
3838
use Exception;
39+
use Google\Cloud\Dev\Component;
3940

4041
/**
4142
* Add a Component
@@ -57,24 +58,18 @@ class NewComponentCommand extends Command
5758
'phpunit.xml.dist.twig',
5859
'README.md.twig',
5960
];
60-
private const BAZEL_VERSION = '6.0.0';
61-
private const OWLBOT_CLI_IMAGE = 'gcr.io/cloud-devrel-public-resources/owlbot-cli:latest';
62-
private const OWLBOT_PHP_IMAGE = 'gcr.io/cloud-devrel-public-resources/owlbot-php:latest';
6361

6462
private $rootPath;
6563
private $httpClient;
66-
private RunProcess $runProcess;
6764

6865
/**
6966
* @param string $rootPath The path to the repository root directory.
7067
* @param Client $httpClient specify the HTTP client, useful for tests.
71-
* @param RunProcess $runProcess Instance to execute Symfony Process commands, useful for tests.
7268
*/
73-
public function __construct($rootPath, ?Client $httpClient = null, ?RunProcess $runProcess = null)
69+
public function __construct($rootPath, ?Client $httpClient = null)
7470
{
7571
$this->rootPath = realpath($rootPath);
7672
$this->httpClient = $httpClient ?: new Client();
77-
$this->runProcess = $runProcess ?: new RunProcess();
7873
parent::__construct();
7974
}
8075

@@ -105,14 +100,25 @@ protected function execute(InputInterface $input, OutputInterface $output)
105100
$new = NewComponent::fromProto($this->loadProtoContent($proto), $protoFile);
106101
$new->componentPath = $this->rootPath;
107102

108-
$unsafeTimeout = $input->getOption('timeout');
103+
$existingComponent = null;
104+
if ($components = Component::getComponents([$new->componentName])) {
105+
// component already exists
106+
$existingComponent = array_pop($components);
107+
$output->writeln(''); // blank line
108+
if (!$this->getHelper('question')->ask($input, $output, new ConfirmationQuestion(
109+
sprintf('Component %s already exists. Overwrite it? [Y/n]', $existingComponent->getName()),
110+
'Y'
111+
))) {
112+
return 0;
113+
}
114+
}
109115

116+
$unsafeTimeout = $input->getOption('timeout');
110117
if (!is_numeric($unsafeTimeout)) {
111118
throw new RuntimeException(
112119
'Error: The timeout option must be a positive integer'
113120
);
114121
}
115-
116122
$timeout = (int) $unsafeTimeout;
117123

118124
$output->writeln(''); // blank line

dev/src/Command/UpdateComponentCommand.php

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\Yaml\Yaml;
2727
use RuntimeException;
2828
use Exception;
29+
use Google\Cloud\Dev\Component;
2930

3031
/**
3132
* Update Component Command
@@ -103,27 +104,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
103104
$this->checkDockerAvailable();
104105

105106
// Find components to update
106-
$components = $this->findComponents($componentName);
107-
108-
if (empty($components)) {
109-
if ($componentName) {
110-
throw new RuntimeException("Component '$componentName' not found.");
111-
} else {
112-
throw new RuntimeException("No components found.");
113-
}
114-
}
115-
107+
$components = Component::getComponents($componentName ? [$componentName] : []);
116108
foreach ($components as $component) {
117-
$output->writeln("\n<info>Running Owlbot in $component</info>");
109+
$componentName = $component->getName();
110+
$output->writeln("\n<info>Running Owlbot in $componentName</info>");
118111

119112
// Copy code from googleapis-gen
120113
$output->writeln("Copying code from googleapis-gen...");
121-
$result = $this->owlbotCopyCode($component, $googleApisGenDir);
114+
$result = $this->owlbotCopyCode($componentName, $googleApisGenDir);
122115
$output->writeln($result);
123116

124117
// Copy bazel-bin files
125118
$output->writeln("Copying bazel-bin files...");
126-
$result = $this->owlbotCopyBazelBin($component, $googleApisGenDir);
119+
$result = $this->owlbotCopyBazelBin($componentName, $googleApisGenDir);
127120
$output->writeln($result);
128121
}
129122

@@ -137,35 +130,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
137130
return Command::SUCCESS;
138131
}
139132

140-
/**
141-
* Find components to update based on the provided component name.
142-
*
143-
* @param string|null $componentName Optional component name to filter by.
144-
* @return array List of component names to update.
145-
*/
146-
private function findComponents(?string $componentName): array
147-
{
148-
$command = ['find', $this->rootPath, '-mindepth', '1', '-maxdepth', '1', '-type', 'd'];
149-
150-
if ($componentName) {
151-
$command[] = '-name';
152-
$command[] = $componentName;
153-
}
154-
155-
$command[] = '-printf';
156-
$command[] = '%f\n';
157-
158-
$output = $this->runProcess->execute($command, null, $this->timeout);
159-
160-
// Filter to only include directories that start with an uppercase letter
161-
$components = array_filter(
162-
explode("\n", $output),
163-
fn($dir) => $dir && preg_match('/^[A-Z]/', $dir)
164-
);
165-
166-
return $components;
167-
}
168-
169133
/**
170134
* Run Owlbot copy-code command to copy code from googleapis-gen.
171135
*

dev/src/Composer.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ public function updateMainComposer()
7272
$composer = json_decode(file_get_contents($rootComposer), true);
7373

7474
// Add `replace` to main composer file.
75-
$composer['replace'][$this->composerPackage] = '0.0.0';
76-
ksort($composer['replace']);
75+
if (!isset($composer['replace'][$this->composerPackage])) {
76+
$composer['replace'][$this->composerPackage] = '0.0.0';
77+
ksort($composer['replace']);
78+
}
7779

7880
// Add namespaces to main composer file.
7981
$composer['autoload']['psr-4'][$this->phpNamespace . '\\'] = $this->relativePath . '/src';
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace Google\Cloud\Dev\Tests\Unit\Command;
4+
5+
use Google\Cloud\Dev\Command\AddVersionCommand;
6+
use Google\Cloud\Dev\Component;
7+
use PHPUnit\Framework\TestCase;
8+
use Symfony\Component\Console\Application;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Tester\CommandTester;
11+
use Symfony\Component\Filesystem\Filesystem;
12+
13+
class AddVersionCommandTest extends TestCase
14+
{
15+
private static $rootPath;
16+
private static $componentPath;
17+
private static $owlbotFile;
18+
19+
public static function setUpBeforeClass(): void
20+
{
21+
self::$rootPath = sys_get_temp_dir() . '/google-cloud-php-tests';
22+
self::$componentPath = self::$rootPath . '/component';
23+
self::$owlbotFile = self::$componentPath . '/.OwlBot.yaml';
24+
$filesystem = new Filesystem();
25+
$filesystem->mirror(__DIR__ . '/../../fixtures/component', self::$componentPath);
26+
}
27+
28+
public static function tearDownAfterClass(): void
29+
{
30+
$filesystem = new Filesystem();
31+
$filesystem->remove(self::$rootPath);
32+
}
33+
34+
public function testAddVersion()
35+
{
36+
$command = new AddVersionCommand(self::$rootPath);
37+
$command->setApplication($this->mockApplication());
38+
$tester = new CommandTester($command);
39+
40+
$tester->execute([
41+
'component' => 'component',
42+
'version' => 'v2',
43+
]);
44+
45+
$this->assertStringContainsString('Adding new version \'v2\' to .OwlBot.yaml', $tester->getDisplay());
46+
$this->assertStringContainsString('(v1|v1beta1|v2)', file_get_contents(self::$owlbotFile));
47+
}
48+
49+
public function testAddVersionNoUpdate()
50+
{
51+
$command = new AddVersionCommand(self::$rootPath);
52+
$command->setApplication($this->mockApplication(false));
53+
$tester = new CommandTester($command);
54+
55+
$tester->execute([
56+
'component' => 'component',
57+
'version' => 'v3',
58+
'--no-update-component' => true,
59+
]);
60+
61+
$this->assertStringContainsString('Adding new version \'v3\' to .OwlBot.yaml', $tester->getDisplay());
62+
$this->assertStringContainsString('(v1|v1beta1|v2|v3)', file_get_contents(self::$owlbotFile));
63+
$this->assertStringContainsString('Skipping component update', $tester->getDisplay());
64+
}
65+
66+
public function testDoesNotUpdateOwlBotIfVersionExists()
67+
{
68+
$command = new AddVersionCommand(self::$rootPath);
69+
$command->setApplication($this->mockApplication());
70+
$tester = new CommandTester($command);
71+
72+
$tester->execute([
73+
'component' => 'component',
74+
'version' => 'v1beta1',
75+
]);
76+
77+
$this->assertStringContainsString('Adding new version \'v1beta1\' to .OwlBot.yaml', $tester->getDisplay());
78+
$this->assertStringContainsString('Version \'v1beta1\' already exists in deep-copy-regex', $tester->getDisplay());
79+
$this->assertStringContainsString('(v1|v1beta1|v2|v3)', file_get_contents(self::$owlbotFile));
80+
}
81+
82+
private function mockApplication(bool $shouldCallUpdate = true): Application
83+
{
84+
$updateCommand = $this->createMock(Command::class);
85+
$updateCommand->expects($shouldCallUpdate ? $this->exactly(2) : $this->never())
86+
->method('run')
87+
->willReturn(0);
88+
89+
$application = $this->createMock(Application::class);
90+
$application->method('has')->willReturn(true);
91+
$application->method('find')->willReturn($updateCommand);
92+
93+
return $application;
94+
}
95+
}

0 commit comments

Comments
 (0)