diff --git a/.travis.yml b/.travis.yml index fd8876ac630c..0c37a6d81b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,31 @@ language: php - sudo: required dist: trusty matrix: - include: - - php: 5.5.38 - - php: 5.6.25 - - php: 7.0 - - php: 7.1 - - php: hhvm - group: edge - fast_finish: true + include: + - php: 5.5.38 + - php: 5.6.25 + - php: 7.0 + - php: 7.1 + - php: hhvm + group: edge + fast_finish: true before_script: - - pecl install grpc || echo 'Failed to install grpc' - - composer install + - pecl install grpc || echo 'Failed to install grpc' + - composer install script: - - ./dev/sh/tests - - vendor/bin/phpcs --standard=./phpcs-ruleset.xml - - ./dev/sh/build-docs + - ./dev/sh/tests + - vendor/bin/phpcs --standard=./phpcs-ruleset.xml + - ./dev/sh/build-docs after_success: - - bash <(curl -s https://codecov.io/bash) - - ./dev/sh/push-docs + - bash <(curl -s https://codecov.io/bash) + - ./dev/sh/push-docs + - ./dev/sh/trigger-split + - cat ./build/snippets-uncovered.json after_failure: - - echo "SNIPPET COVERAGE REPORT" && cat ./build/snippets-uncovered.json + - echo "SNIPPET COVERAGE REPORT" && cat ./build/snippets-uncovered.json diff --git a/composer.json b/composer.json index 4402d1145b28..0ed669718bb4 100644 --- a/composer.json +++ b/composer.json @@ -76,5 +76,13 @@ }, "scripts": { "google-cloud": "dev/google-cloud" + }, + "extra": { + "component": { + "id": "google-cloud", + "target": "git@github.com:jdpedrie-gcp/google-cloud-php.git", + "path": "src", + "entry": "ServiceBuilder.php" + } } } diff --git a/dev/google-cloud b/dev/google-cloud index cd0636602a4f..e004b499d438 100755 --- a/dev/google-cloud +++ b/dev/google-cloud @@ -20,6 +20,7 @@ require __DIR__ . '/../vendor/autoload.php'; use Google\Cloud\Dev\DocGenerator\Command\Docs; use Google\Cloud\Dev\Release\Command\Release; +use Google\Cloud\Dev\Split\Command\Split; use Symfony\Component\Console\Application; if (!class_exists(Application::class)) { @@ -33,4 +34,5 @@ if (!class_exists(Application::class)) { $app = new Application; $app->add(new Release(__DIR__)); $app->add(new Docs(__DIR__)); +$app->add(new Split(__DIR__)); $app->run(); diff --git a/dev/sh/build-docs b/dev/sh/build-docs index cc09be67d534..1079b9de9dd6 100755 --- a/dev/sh/build-docs +++ b/dev/sh/build-docs @@ -5,7 +5,13 @@ set -ev function buildDocs () { echo "doc dir before generation:" find docs - composer google-cloud docs + + if [ -z "$TRAVIS_TAG" ]; then + ./dev/google-cloud docs + else + ./dev/google-cloud docs -r ${TRAVIS_TAG} + fi + echo "doc dir after generation:" find docs } diff --git a/dev/sh/compile-splitsh b/dev/sh/compile-splitsh new file mode 100755 index 000000000000..1cef5094d2b0 --- /dev/null +++ b/dev/sh/compile-splitsh @@ -0,0 +1,13 @@ +#!/bin/bash + +mkdir $ +export GOPATH=$TRAVIS_BUILD_DIR/go + +go get -d github.com/libgit2/git2go +cd $GOPATH/src/github.com/libgit2/git2go +git checkout next +git submodule update --init +make install + +go get github.com/splitsh/lite +go build -o $TRAVIS_BUILD_DIR/splitsh-lite github.com/splitsh/lite diff --git a/dev/sh/push-docs b/dev/sh/push-docs index a0ef3d50e770..0ee349729324 100755 --- a/dev/sh/push-docs +++ b/dev/sh/push-docs @@ -2,20 +2,11 @@ set -ev -function generateDocs () { - echo "doc dir before generation:" - find docs - composer google-cloud docs - echo "doc dir after generation:" - find docs -} - function pushDocs () { - git submodule add -q -f -b gh-pages https://${GH_OAUTH_TOKEN}@github.com/${GH_OWNER}/${GH_PROJECT_NAME} ghpages - mkdir -p ghpages/json/${1} - cp -R docs/json/master/* ghpages/json/${1} - cp docs/overview.html ghpages/json/${1} - cp docs/toc.json ghpages/json/${1} + git submodule add -q -f -b gh-pages https://${GH_OAUTH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} ghpages + + rsync -aP docs/json/* ghpages/json/ + cp docs/home.html ghpages/json cp docs/manifest.json ghpages cd ghpages @@ -25,7 +16,7 @@ function pushDocs () { git config user.email "travis@travis-ci.org" git commit -m "Updating docs for ${1}" git status - git push -q https://${GH_OAUTH_TOKEN}@github.com/${GH_OWNER}/${GH_PROJECT_NAME} HEAD:gh-pages + git push -q https://${GH_OAUTH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} HEAD:gh-pages else echo "Nothing to commit." fi diff --git a/dev/sh/split b/dev/sh/split new file mode 100755 index 000000000000..6ff7c259f9ac --- /dev/null +++ b/dev/sh/split @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') + +SHA=`$TRAVIS_BUILD_DIR/splitsh-lite --prefix=$1` +git push -q \ + "https://${GH_OAUTH_TOKEN}@github.com/$2" \ + $SHA:master --force diff --git a/dev/sh/trigger-split b/dev/sh/trigger-split new file mode 100755 index 000000000000..5685a6fe239e --- /dev/null +++ b/dev/sh/trigger-split @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ "$TRAVIS_JOB_NUMBER" == *.1 && -n "$TRAVIS_TAG" ]]; then + $(dirname $0)/compile-splitsh + git fetch --unshallow + composer google-cloud split +else + echo "Split occurs only in a tag run, and in the first matrix build" +fi diff --git a/dev/src/DocGenerator/Command/Docs.php b/dev/src/DocGenerator/Command/Docs.php index 33c522682c84..2ade7f174d00 100644 --- a/dev/src/DocGenerator/Command/Docs.php +++ b/dev/src/DocGenerator/Command/Docs.php @@ -19,7 +19,9 @@ use Google\Cloud\Dev\DocGenerator\DocGenerator; use Google\Cloud\Dev\DocGenerator\GuideGenerator; +use Google\Cloud\Dev\DocGenerator\TableOfContents; use Google\Cloud\Dev\DocGenerator\TypeGenerator; +use Google\Cloud\Dev\GetComponentsTrait; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveRegexIterator; @@ -27,11 +29,16 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Docs extends Command { - const DEFAULT_OUTPUT_DIR = 'docs/json/master'; + use GetComponentsTrait; + + const DEFAULT_OUTPUT_DIR = 'docs/json'; + const TOC_SOURCE_DIR = 'docs/contents'; + const TOC_TEMPLATE = 'docs/toc.json'; const DEFAULT_SOURCE_DIR = 'src'; private $cliBasePath; @@ -47,29 +54,94 @@ protected function configure() { $this->setName('docs') ->setDescription('Generate Documentation') - ->addArgument('source', InputArgument::OPTIONAL, 'The source directory to traverse and parse') - ->addArgument('output', InputArgument::OPTIONAL, 'The directory to output files into'); + ->addOption('release', 'r', InputOption::VALUE_REQUIRED, 'If set, docs will be generated into tag folders' . + ' such as v1.0.0 rather than master.', false) + ->addOption('pretty', 'p', InputOption::VALUE_OPTIONAL, 'If set, json files will be written with pretty'. + ' formatting using PHP\'s JSON_PRETTY_PRINT flag', false); } protected function execute(InputInterface $input, OutputInterface $output) { + $release = ($input->getOption('release') === false && $input->getOption('release') !== 'false') + ? null + : $input->getOption('release'); + + $pretty = ($input->getOption('pretty') === false) ? false : true; + $paths = [ - 'source' => ($input->getArgument('source')) - ? $this->cliBasePath .'/../'. $input->getArgument('source') - : $this->cliBasePath .'/../'. self::DEFAULT_SOURCE_DIR, + 'source' => $this->cliBasePath .'/../'. self::DEFAULT_SOURCE_DIR, + 'output' => $this->cliBasePath .'/../'. self::DEFAULT_OUTPUT_DIR, + 'project' => $this->cliBasePath .'/../', + 'manifest' => $this->cliBasePath .'/../docs/manifest.json', + 'toc' => $this->cliBasePath .'/../'. self::TOC_SOURCE_DIR, + 'tocTemplate' => $this->cliBasePath .'/../'. self::TOC_TEMPLATE + ]; - 'output' => ($input->getArgument('output')) - ? $this->cliBasePath .'/../'. $input->getArgument('output') - : $this->cliBasePath .'/../'. self::DEFAULT_OUTPUT_DIR + $components = $this->getComponents($paths['source']); + $tocTemplate = json_decode(file_get_contents($paths['tocTemplate']), true); + + foreach ($components as $component) { + $input = $paths['project'] . $component['path']; + $source = $this->getFilesList($input); + $this->generateComponentDocumentation($output, $source, $component, $paths, $tocTemplate, $release, $pretty); + } + + $source = [$paths['project'] .'src/ServiceBuilder.php']; + $component = [ + 'id' => 'google-cloud', + 'path' => 'src/' ]; + $this->generateComponentDocumentation($output, $source, $component, $paths, $tocTemplate, $release, $pretty); + } + + private function generateComponentDocumentation( + OutputInterface $output, + array $source, + array $component, + array $paths, + $tocTemplate, + $release = false, + $pretty = false + ) { + $output->writeln(sprintf('Writing documentation for %s', $component['id'])); + $output->writeln('--------------'); + + $version = $this->getComponentVersion($paths['manifest'], $component['id']); + + $outputPath = ($release) + ? $paths['output'] .'/'. $component['id'] .'/'. $version + : $paths['output'] .'/'. $component['id'] .'/master'; + + $output->writeln(sprintf('Writing to %s', realpath($outputPath))); + + $types = new TypeGenerator($outputPath); + + $docs = new DocGenerator( + $types, + $source, + $outputPath, + $this->cliBasePath, + $component['id'], + $paths['manifest'], + $release + ); + $docs->generate($component['path'], $pretty); + + $types->write($pretty); - $types = new TypeGenerator($paths['output']); + $output->writeln(sprintf('Writing table of contents to %s', realpath($outputPath))); + $services = json_decode(file_get_contents($paths['toc'] .'/'. $component['id'] .'.json'), true); - $sourceFiles = $this->getFilesList($paths['source']); - $docs = new DocGenerator($types, $sourceFiles, $paths['output'], $this->cliBasePath); - $docs->generate(); + $toc = new TableOfContents( + $tocTemplate, + $services, + $release, + $outputPath + ); + $toc->generate($pretty); - $types->write(); + $output->writeln(' '); + $output->writeln(' '); } private function getFilesList($source) diff --git a/dev/src/DocGenerator/DocGenerator.php b/dev/src/DocGenerator/DocGenerator.php index 7394002af896..239e54eb1dca 100644 --- a/dev/src/DocGenerator/DocGenerator.php +++ b/dev/src/DocGenerator/DocGenerator.php @@ -32,16 +32,29 @@ class DocGenerator private $files; private $outputPath; private $executionPath; + private $componentId; + private $manifestPath; + private $release; /** * @param array $files */ - public function __construct(TypeGenerator $types, array $files, $outputPath, $executionPath) - { + public function __construct( + TypeGenerator $types, + array $files, + $outputPath, + $executionPath, + $componentId, + $manifestPath, + $release + ) { $this->types = $types; $this->files = $files; $this->outputPath = $outputPath; $this->executionPath = $executionPath; + $this->componentId = $componentId; + $this->manifestPath = $manifestPath; + $this->release = $release; } /** @@ -49,31 +62,56 @@ public function __construct(TypeGenerator $types, array $files, $outputPath, $ex * * @return void */ - public function generate() + public function generate($basePath, $pretty) { foreach ($this->files as $file) { - $currentFile = substr(str_replace($this->executionPath, '', $file), 3); + if ($basePath) { + $currentFileArr = explode($basePath, trim($file, '/')); + if (isset($currentFileArr[1])) { + $currentFile = trim($currentFileArr[1], '/'); + } + } + $isPhp = strrpos($file, '.php') == strlen($file) - strlen('.php'); if ($isPhp) { $fileReflector = new FileReflector($file); - $parser = new CodeParser($file, $currentFile, $fileReflector); + $parser = new CodeParser( + $file, + $currentFile, + $fileReflector, + dirname($this->executionPath), + $this->componentId, + $this->manifestPath, + $this->release + ); } else { $content = file_get_contents($file); - $parser = new MarkdownParser($currentFile, $content); + $split = explode('src/', $file); + $parser = new MarkdownParser($split[1], $content); } $document = $parser->parse(); - $writer = new Writer(json_encode($document), $this->outputPath); - $writer->write(substr($currentFile, 4)); + $writer = new Writer($document, $this->outputPath, $pretty); + $writer->write($currentFile); $this->types->addType([ 'id' => $document['id'], 'title' => $document['title'], - 'contents' => $document['id'] . '.json' + 'contents' => $this->prune($document['id'] . '.json') ]); } } + + private function prune($contentsFileName) + { + $explode = explode('/', $contentsFileName); + if (count($explode) > 1) { + array_shift($explode); + } + + return implode('/', $explode); + } } diff --git a/dev/src/DocGenerator/Parser/CodeParser.php b/dev/src/DocGenerator/Parser/CodeParser.php index e478bdd41d42..fd13c43f8885 100644 --- a/dev/src/DocGenerator/Parser/CodeParser.php +++ b/dev/src/DocGenerator/Parser/CodeParser.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Dev\DocGenerator\Parser; use Google\Cloud\Dev\DocBlockStripSpaces; +use Google\Cloud\Dev\GetComponentsTrait; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Description; use phpDocumentor\Reflection\DocBlock\Tag\SeeTag; @@ -25,21 +26,38 @@ class CodeParser implements ParserInterface { + use GetComponentsTrait; + const SNIPPET_NAME_REGEX = '/\/\/\s?\[snippet\=(\w{0,})\]/'; private $path; private $outputName; private $reflector; private $markdown; + private $projectRoot; private $externalTypes; - - public function __construct($path, $outputName, FileReflector $reflector) - { + private $componentId; + private $manifestPath; + private $release; + + public function __construct( + $path, + $outputName, + FileReflector $reflector, + $projectRoot, + $componentId, + $manifestPath, + $release + ) { $this->path = $path; $this->outputName = $outputName; $this->reflector = $reflector; $this->markdown = \Parsedown::instance(); + $this->projectRoot = $projectRoot; $this->externalTypes = json_decode(file_get_contents(__DIR__ .'/../../../../docs/external-classes.json'), true); + $this->componentId = $componentId; + $this->manifestPath = $manifestPath; + $this->release = $release; } public function parse() @@ -485,15 +503,59 @@ private function buildExternalType($type) private function buildLink($content) { - if ($content[0] === '\\') { - $content = substr($content, 1); + $componentId = null; + if (substr_compare(trim($content, '\\'), 'Google\Cloud', 0, 12) === 0) { + try { + $matches = []; + preg_match('/[Generator\<]?(Google\\\Cloud\\\[\w\\\]{0,})[\>]?[\[\]]?/', $content, $matches); + $ref = new \ReflectionClass($matches[1]); + } catch (\ReflectionException $e) { + throw new \Exception(sprintf( + 'Reflection Exception: %s in %s. Given class was %s', + $e->getMessage(), + realpath($this->path), + $content + )); + } + + $recurse = true; + $file = $ref->getFileName(); + + if (strpos($file, dirname(realpath($this->path))) !== false) { + $recurse = false; + } + + do { + $composer = dirname($file) .'/composer.json'; + if (file_exists($composer) && $component = $this->isComponent($composer)) { + $componentId = $component['id']; + if ($componentId === $this->componentId) { + $componentId = null; + } + $recurse = false; + } elseif (trim($file, '/') === trim($this->projectRoot, '/')) { + $recurse = false; + } else { + $file = dirname($file); + } + } while($recurse); } + $content = trim($content, '\\'); + $displayName = $content; $content = substr($content, 13); $parts = explode('::', $content); $type = strtolower(str_replace('\\', '/', $parts[0])); + if ($componentId) { + $version = ($this->release) + ? $this->getComponentVersion($this->manifestPath, $componentId) + : 'master'; + + $type = $componentId .'/'. $version .'/'. $type; + } + $openTag = ' $examples ]; } + + private static $composerFiles = []; + + private function isComponent($composerPath) + { + if (isset(self::$composerFiles[$composerPath])) { + $contents = self::$composerFiles[$composerPath]; + } else { + $contents = json_decode(file_get_contents($composerPath), true); + self::$composerFiles[$composerPath] = $contents; + } + + if (isset($contents['extra']['component'])) { + return $contents['extra']['component']; + } + + return false; + } } diff --git a/dev/src/DocGenerator/Parser/MarkdownParser.php b/dev/src/DocGenerator/Parser/MarkdownParser.php index 2599f80d4954..058dba85bbfe 100644 --- a/dev/src/DocGenerator/Parser/MarkdownParser.php +++ b/dev/src/DocGenerator/Parser/MarkdownParser.php @@ -49,7 +49,7 @@ public function parse() $body = $doc->getElementsByTagName('body')->item(0); return [ - 'id' => strtolower(substr($pathinfo['dirname'] .'/'. $pathinfo['filename'], 5)), + 'id' => strtolower(trim($pathinfo['dirname'] .'/'. $pathinfo['filename'], '/.')), 'type' => 'guide', 'title' => $heading->textContent, 'name' => $heading->textContent, diff --git a/dev/src/DocGenerator/TableOfContents.php b/dev/src/DocGenerator/TableOfContents.php new file mode 100644 index 000000000000..162a5877c8be --- /dev/null +++ b/dev/src/DocGenerator/TableOfContents.php @@ -0,0 +1,44 @@ +template = $template; + $this->component = $component; + $this->componentVersion = $componentVersion; + $this->outputPath = $outputPath; + } + + public function generate($pretty = false) + { + $toc = $this->template; + $toc['services'] = $this->component; + $toc['tagName'] = $this->componentVersion; + + $writer = new Writer($toc, $this->outputPath, $pretty); + $writer->write('toc.json'); + } +} diff --git a/dev/src/DocGenerator/TypeGenerator.php b/dev/src/DocGenerator/TypeGenerator.php index f05a39a6fcfc..8abfc8d4ac87 100644 --- a/dev/src/DocGenerator/TypeGenerator.php +++ b/dev/src/DocGenerator/TypeGenerator.php @@ -36,9 +36,9 @@ public function addType(array $type) $this->types[] = $type; } - public function write() + public function write($pretty = false) { - $writer = new Writer(json_encode($this->types), $this->outputPath); + $writer = new Writer($this->types, $this->outputPath, $pretty); $writer->write('types.json'); } } diff --git a/dev/src/DocGenerator/Writer.php b/dev/src/DocGenerator/Writer.php index 4cafcb9200e3..f2c22e458b63 100644 --- a/dev/src/DocGenerator/Writer.php +++ b/dev/src/DocGenerator/Writer.php @@ -21,11 +21,13 @@ class Writer { private $content; private $outputPath; + private $pretty; - public function __construct($content, $outputPath) + public function __construct(array $content, $outputPath, $pretty = false) { $this->content = $content; $this->outputPath = $outputPath; + $this->pretty = (bool) $pretty; } public function write($currentFile) @@ -33,10 +35,14 @@ public function write($currentFile) $path = $this->buildOutputPath($currentFile); if (!is_dir(dirname($path))) { - mkdir(dirname($path), 0777, true); + @mkdir(dirname($path), 0777, true); } - file_put_contents($path, $this->content); + $content = ($this->pretty) + ? json_encode($this->content, JSON_PRETTY_PRINT) + : json_encode($this->content); + + file_put_contents($path, $content); } private function buildOutputPath($currentFile) diff --git a/dev/src/GetComponentsTrait.php b/dev/src/GetComponentsTrait.php new file mode 100644 index 000000000000..3efa11290fe5 --- /dev/null +++ b/dev/src/GetComponentsTrait.php @@ -0,0 +1,116 @@ + 1) { + $component['prefix'] = dirname('src' . $path[1]); + } else { + $component['prefix'] = ''; + } + + $components[] = $component; + } + + return $components; + } + + private function getComponentVersion($manifestPath, $componentId) + { + $manifest = $this->getComponentManifest($manifestPath, $componentId); + return $manifest['versions'][0]; + } + + private function getComponentManifest($manifestPath, $componentId) + { + $manifest = $this->getManifest($manifestPath); + $index = $this->getManifestComponentModuleIndex($manifestPath, $manifest, $componentId); + + return $manifest['modules'][$index]; + } + + private function getManifestComponentModuleIndex($manifestPath, array $manifest, $componentId) + { + $modules = array_filter($this->getManifest($manifestPath)['modules'], function ($module) use ($componentId) { + return ($module['id'] === $componentId); + }); + + return array_keys($modules)[0]; + } + + private function getManifest($manifestPath) + { + if (self::$__manifest) { + $manifest = self::$__manifest; + } else { + $manifest = json_decode(file_get_contents($manifestPath), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not decode manifest json'); + } + + self::$__manifest = $manifest; + } + + return $manifest; + } + + private function getComponentComposer($componentId) + { + $components = $this->getComponents($this->components, $this->defaultComponentComposer); + + $components = array_values(array_filter($components, function ($component) use ($componentId) { + return ($component['id'] === $componentId); + })); + + if (count($components) === 0) { + throw new \InvalidArgumentException(sprintf( + 'Given component id %s is not a valid component.', + $componentId + )); + } + + return $components[0]; + } +} diff --git a/dev/src/Release/Command/Release.php b/dev/src/Release/Command/Release.php index edc3bd1f21c3..37e0b62400c8 100644 --- a/dev/src/Release/Command/Release.php +++ b/dev/src/Release/Command/Release.php @@ -17,20 +17,35 @@ namespace Google\Cloud\Dev\Release\Command; +use Google\Cloud\Dev\GetComponentsTrait; use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use vierbergenlars\SemVer\version; class Release extends Command { - const PATH_MANIFEST = 'docs/manifest.json'; - const PATH_SERVICE_BUILDER = 'src/ServiceBuilder.php'; + use GetComponentsTrait; + + const COMPONENT_BASE = '%s/../src'; + const DEFAULT_COMPONENT = 'google-cloud'; + const DEFAULT_COMPONENT_COMPOSER = '%s/../composer.json'; + const PATH_MANIFEST = '%s/../docs/manifest.json'; + const PATH_SERVICE_BUILDER = '%s/../src/ServiceBuilder.php'; private $cliBasePath; + private $defaultClient; + + private $manifest; + + private $defaultComponentComposer; + + private $components; + private $allowedReleaseTypes = [ 'major', 'minor', 'patch' ]; @@ -39,6 +54,11 @@ public function __construct($cliBasePath) { $this->cliBasePath = $cliBasePath; + $this->defaultClient = sprintf(self::PATH_SERVICE_BUILDER, $cliBasePath); + $this->manifest = sprintf(self::PATH_MANIFEST, $cliBasePath); + $this->defaultComponentComposer = sprintf(self::DEFAULT_COMPONENT_COMPOSER, $cliBasePath); + $this->components = sprintf(self::COMPONENT_BASE, $cliBasePath); + parent::__construct(); } @@ -46,23 +66,31 @@ protected function configure() { $this->setName('release') ->setDescription('Prepares a new release') - ->addArgument('version', InputArgument::REQUIRED, 'The new version number'); + ->addArgument('version', InputArgument::REQUIRED, 'The new version number.') + ->addOption( + 'component', + 'c', + InputOption::VALUE_REQUIRED, + 'The component for which the version should be updated.', + self::DEFAULT_COMPONENT + ); } protected function execute(InputInterface $input, OutputInterface $output) { + $component = $this->getComponentComposer($input->getOption('component')); + $version = $input->getArgument('version'); + + // If the version is one of "major", "minor" or "patch", determine the + // correct incrementation. if (in_array(strtolower($version), $this->allowedReleaseTypes)) { - $version = $this->getNextVersionName($version); + $version = $this->getNextVersionName($version, $component); } try { $validatedVersion = new version($version); } catch (\Exception $e) { - $validatedVersion = null; - } - - if (is_null($validatedVersion)) { throw new RuntimeException(sprintf( 'Given version %s is not a valid version name', $version @@ -72,18 +100,33 @@ protected function execute(InputInterface $input, OutputInterface $output) $version = (string) $validatedVersion; $output->writeln(sprintf( - 'Adding version %s to Documentation Manifest.', - $version + 'Adding version %s to Documentation Manifest for component %s.', + $version, + $component['id'] )); - $this->addToManifest($version); + $this->addToComponentManifest($version, $component); $output->writeln(sprintf( - 'Setting ServiceBuilder version constant to %s.', + 'Setting component version constant to %s.', $version )); - $this->updateServiceBuilder($version); + $this->updateComponentVersionConstant($version, $component); + $output->writeln(sprintf( + 'File %s VERSION constant updated to %s', + $component['entry'], + $version + )); + + if ($component['id'] !== 'google-cloud') { + $this->updateComponentVersionFile($version, $component); + $output->writeln(sprintf( + 'Component %s VERSION file updated to %s', + $component['id'], + $version + )); + } $output->writeln(sprintf( 'Release %s generated!', @@ -91,64 +134,72 @@ protected function execute(InputInterface $input, OutputInterface $output) )); } - private function getNextVersionName($type) + private function getNextVersionName($type, array $component) { - $manifest = $this->getManifest(); - $lastRelease = new version($manifest['versions'][0]); + $manifest = $this->getComponentManifest($this->manifest, $component['id']); + + if ($manifest['versions'][0] === 'master') { + $lastRelease = new version('0.0.0'); + } else { + $lastRelease = new version($manifest['versions'][0]); + } return $lastRelease->inc($type); } - private function addToManifest($version) + private function addToComponentManifest($version, array $component) { - $manifest = $this->getManifest(); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new RuntimeException('Could not decode manifest json'); - } + $manifest = $this->getManifest($this->manifest); + $index = $this->getManifestComponentModuleIndex($this->manifest, $manifest, $component['id']); - array_unshift($manifest['versions'], 'v'. $version); + array_unshift($manifest['modules'][$index]['versions'], 'v'. $version); $content = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ."\n"; - $result = file_put_contents($this->getManifestPath(), $content); + $result = file_put_contents($this->manifest, $content); if (!$result) { throw new RuntimeException('File write failed'); } } - private function updateServiceBuilder($version) + private function updateComponentVersionConstant($version, array $component) { - $path = $this->cliBasePath .'/../'. self::PATH_SERVICE_BUILDER; + if (is_null($component['entry'])) { + return false; + } + + $path = $this->cliBasePath .'/../'. $component['path'] .'/'. $component['entry']; if (!file_exists($path)) { - throw new RuntimeException('ServiceBuilder not found at '. $path); + throw new \RuntimeException(sprintf( + 'Component entry file %s does not exist', + $path + )); } - $sb = file_get_contents($path); + $entry = file_get_contents($path); $replacement = sprintf("const VERSION = '%s';", $version); - $sb = preg_replace("/const VERSION = '[0-9.]{0,}'\;/", $replacement, $sb); + $entry = preg_replace("/const VERSION = [\'\\\"]([0-9.]{0,}|master)[\'\\\"]\;/", $replacement, $entry); - $result = file_put_contents($path, $sb); + $result = file_put_contents($path, $entry); if (!$result) { throw new RuntimeException('File write failed'); } + + return true; } - private function getManifest() + private function updateComponentVersionFile($version, array $component) { - $path = $this->getManifestPath(); - if (!file_exists($path)) { - throw new RuntimeException('Manifest file not found at '. $path); - } + $path = $this->cliBasePath .'/../'. $component['path'] .'/VERSION'; + $result = file_put_contents($path, $version); - return json_decode(file_get_contents($path), true); - } + if (!$result) { + throw new RuntimeException('File write failed'); + } - private function getManifestPath() - { - return $this->cliBasePath .'/../'. self::PATH_MANIFEST; + return true; } } diff --git a/dev/src/Snippet/Parser/Snippet.php b/dev/src/Snippet/Parser/Snippet.php index 74316ecaf25d..d1ace59affdf 100644 --- a/dev/src/Snippet/Parser/Snippet.php +++ b/dev/src/Snippet/Parser/Snippet.php @@ -152,9 +152,14 @@ public function invoke($returnVar = null) $cb = function($return) use ($content) { extract($this->locals); - ob_start(); - $res = eval($content ."\n\n". $return); - $out = ob_get_clean(); + try { + ob_start(); + $res = eval($content ."\n\n". $return); + $out = ob_get_clean(); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } return new InvokeResult($res, $out); }; diff --git a/dev/src/Split/Command/Split.php b/dev/src/Split/Command/Split.php new file mode 100644 index 000000000000..6959e6985ac7 --- /dev/null +++ b/dev/src/Split/Command/Split.php @@ -0,0 +1,172 @@ +cliBasePath = $cliBasePath; + $this->splitShell = sprintf(self::SPLIT_SHELL, $cliBasePath); + $this->components = sprintf(self::COMPONENT_BASE, $cliBasePath); + $this->manifest = sprintf(self::PATH_MANIFEST, $cliBasePath); + + $this->http = new Client; + $this->token = getenv(self::TOKEN_ENV); + + parent::__construct(); + } + + protected function configure() + { + $this->setName('split') + ->setDescription('Split subtree and push to various remotes.'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!getenv(self::TAG_ENV)) { + $output->writeln('This command should only be run inside a CI post-release process'); + return; + } + + $components = $this->getComponents($this->components); + + $tag = getenv(self::TAG_ENV); + + $parentTagSource = sprintf(self::PARENT_TAG_NAME, $tag); + + foreach ($components as $component) { + $output->writeln(''); + $output->writeln(sprintf('Starting on component %s', $component['id'])); + $output->writeln('------------'); + shell_exec(sprintf( + '%s %s %s', + $this->splitShell, + $component['prefix'], + $component['target'] + )); + + $target = $component['target']; + $matches = []; + preg_match(self::TARGET_REGEX, $target, $matches); + + $org = $matches[1]; + $repo = $matches[2]; + + $version = $this->getComponentVersion($this->manifest, $component['id']); + try { + new version($version); + } catch (SemVerException $e) { + $output->writeln(sprintf( + 'Component %s version %s is invalid.', + $component['id'], + $version + )); + continue; + } + + if ($this->doesTagExist($version, $org, $repo)) { + $output->writeln(sprintf( + 'Component %s already tagged at version %s', + $component['id'], + $version + )); + continue; + } + + $name = $component['displayName'] .' '. $version; + $notes = sprintf( + 'For release notes, please see the [associated Google Cloud PHP release](%s).', + $parentTagSource + ); + $this->createRelease($version, $org, $repo, $name, $notes); + + $output->writeln(sprintf( + 'Release %s created for component %s', + $version, + $component['id'] + )); + } + } + + private function doesTagExist($tagName, $org, $repo) + { + $res = $this->http->get(sprintf( + self::GITHUB_RELEASES_ENDPOINT, + $org, $repo, $tagName + ), [ + 'http_errors' => false, + 'auth' => [null, $this->token] + ]); + + return ($res->getStatusCode() === 200); + } + + private function createRelease($tagName, $org, $repo, $name, $notes) + { + $requestBody = [ + 'tag_name' => $tagName, + 'name' => $name, + 'body' => $notes + ]; + + $res = $this->http->post(sprintf( + self::GITHUB_RELEASE_CREATE_ENDPOINT, + $org, $repo + ), [ + 'http_errors' => false, + 'json' => $requestBody, + 'auth' => [null, $this->token] + ]); + } +} diff --git a/docs/contents/cloud-bigquery.json b/docs/contents/cloud-bigquery.json new file mode 100644 index 000000000000..1d92ef396619 --- /dev/null +++ b/docs/contents/cloud-bigquery.json @@ -0,0 +1,31 @@ +[{ + "title": "BigQueryClient", + "type": "bigquery/bigqueryclient" +}, { + "title": "Bytes", + "type": "bigquery/bytes" +}, { + "title": "Dataset", + "type": "bigquery/dataset" +}, { + "title": "Date", + "type": "bigquery/date" +}, { + "title": "InsertResponse", + "type": "bigquery/insertresponse" +}, { + "title": "Job", + "type": "bigquery/job" +}, { + "title": "QueryResults", + "type": "bigquery/queryresults" +}, { + "title": "Table", + "type": "bigquery/table" +}, { + "title": "Time", + "type": "bigquery/time" +}, { + "title": "Timestamp", + "type": "bigquery/timestamp" +}] diff --git a/docs/contents/cloud-core.json b/docs/contents/cloud-core.json new file mode 100644 index 000000000000..9779bf4b5c63 --- /dev/null +++ b/docs/contents/cloud-core.json @@ -0,0 +1,30 @@ +[{ + "title": "Overview", + "type": "core/readme" +}, { + "title": "IAM", + "type": "core/iam/iam", + "patterns": [ + "core/iam/\\w{1,}" + ], + "nav": [{ + "title": "PolicyBuilder", + "type": "core/iam/policybuilder" + }] +}, { + "title": "Upload", + "type": "core/upload/abstractuploader", + "nav": [{ + "title": "MultipartUploader", + "type": "core/upload/multipartuploader" + }, { + "title": "ResumableUploader", + "type": "core/upload/resumableuploader" + }, { + "title": "StreamableUploader", + "type": "core/upload/streamableuploader" + }] +}, { + "title": "Int64", + "type": "core/int64" +}] diff --git a/docs/contents/cloud-datastore.json b/docs/contents/cloud-datastore.json new file mode 100644 index 000000000000..aec83962d0d2 --- /dev/null +++ b/docs/contents/cloud-datastore.json @@ -0,0 +1,25 @@ +[{ + "title": "DatastoreClient", + "type": "datastore/datastoreclient" +}, { + "title": "Transaction", + "type": "datastore/transaction" +}, { + "title": "Entity", + "type": "datastore/entity" +}, { + "title": "Key", + "type": "datastore/key" +}, { + "title": "Query", + "type": "datastore/query/query" +}, { + "title": "GQL Query", + "type": "datastore/query/gqlquery" +}, { + "title": "GeoPoint", + "type": "datastore/geopoint" +}, { + "title": "Blob", + "type": "datastore/blob" +}] diff --git a/docs/contents/cloud-error-reporting.json b/docs/contents/cloud-error-reporting.json new file mode 100644 index 000000000000..a6f2d22170da --- /dev/null +++ b/docs/contents/cloud-error-reporting.json @@ -0,0 +1,20 @@ +[{ + "title": "Overview", + "type": "errorreporting/readme" +}, { + "title": "v1beta1", + "type": "errorreporting/v1beta1/readme", + "patterns": [ + "errorreporting/v1beta1/\\w{1,}" + ], + "nav": [{ + "title": "ErrorGroupServiceClient", + "type": "errorreporting/v1beta1/errorgroupserviceclient" + }, { + "title": "ErrorStatsServiceClient", + "type": "errorreporting/v1beta1/errorstatsserviceclient" + }, { + "title": "ReportErrorsServiceClient", + "type": "errorreporting/v1beta1/reporterrorsserviceclient" + }] +}] diff --git a/docs/contents/cloud-logging.json b/docs/contents/cloud-logging.json new file mode 100644 index 000000000000..8b40def8299f --- /dev/null +++ b/docs/contents/cloud-logging.json @@ -0,0 +1,35 @@ +[{ + "title": "LoggingClient", + "type": "logging/loggingclient" +}, { + "title": "Entry", + "type": "logging/entry" +}, { + "title": "Logger", + "type": "logging/logger" +}, { + "title": "Metric", + "type": "logging/metric" +},{ + "title": "PsrLogger", + "type": "logging/psrlogger" +},{ + "title": "Sink", + "type": "logging/sink" +}, { + "title": "v2", + "type": "logging/v2/readme", + "patterns": [ + "logging/v2/\\w{1,}" + ], + "nav": [{ + "title": "ConfigServiceV2Client", + "type": "logging/v2/configservicev2client" + }, { + "title": "LoggingServiceV2Client", + "type": "logging/v2/loggingservicev2client" + }, { + "title": "MetricsServiceV2Client", + "type": "logging/v2/metricsservicev2client" + }] +}] diff --git a/docs/contents/cloud-monitoring.json b/docs/contents/cloud-monitoring.json new file mode 100644 index 000000000000..092ed41613bc --- /dev/null +++ b/docs/contents/cloud-monitoring.json @@ -0,0 +1,17 @@ +[{ + "title": "Overview", + "type": "monitoring/readme" +}, { + "title": "v3", + "type": "monitoring/v3/readme", + "patterns": [ + "monitoring/v3/\\w{1,}" + ], + "nav": [{ + "title": "GroupServiceClient", + "type": "monitoring/v3/groupserviceclient" + }, { + "title": "MetricServiceClient", + "type": "monitoring/v3/metricserviceclient" + }] +}] diff --git a/docs/contents/cloud-natural-language.json b/docs/contents/cloud-natural-language.json new file mode 100644 index 000000000000..c9d82e7b35cd --- /dev/null +++ b/docs/contents/cloud-natural-language.json @@ -0,0 +1,7 @@ +[{ + "title": "NaturalLanguageClient", + "type": "naturallanguage/naturallanguageclient" +}, { + "title": "Annotation", + "type": "naturallanguage/annotation" +}] diff --git a/docs/contents/cloud-pubsub.json b/docs/contents/cloud-pubsub.json new file mode 100644 index 000000000000..ef5acc571971 --- /dev/null +++ b/docs/contents/cloud-pubsub.json @@ -0,0 +1,26 @@ +[{ + "title": "PubSubClient", + "type": "pubsub/pubsubclient" +}, { + "title": "Message", + "type": "pubsub/message" +}, { + "title": "Subscription", + "type": "pubsub/subscription" +}, { + "title": "Topic", + "type": "pubsub/topic" +}, { + "title": "v1", + "type": "pubsub/v1/readme", + "patterns": [ + "pubsub/v1/\\w{1,}" + ], + "nav": [{ + "title": "PublisherClient", + "type": "pubsub/v1/publisherclient" + }, { + "title": "SubscriberClient", + "type": "pubsub/v1/subscriberclient" + }] +}] diff --git a/docs/contents/cloud-speech.json b/docs/contents/cloud-speech.json new file mode 100644 index 000000000000..d433dce34a69 --- /dev/null +++ b/docs/contents/cloud-speech.json @@ -0,0 +1,17 @@ +[{ + "title": "SpeechClient", + "type": "speech/speechclient" +}, { + "title": "Operation", + "type": "speech/operation" +}, { + "title": "v1beta1", + "type": "speech/v1beta1/readme", + "patterns": [ + "speech/v1beta1/\\w{1,}" + ], + "nav": [{ + "title": "SpeechClient", + "type": "speech/v1beta1/speechclient" + }] +}] diff --git a/docs/contents/cloud-storage.json b/docs/contents/cloud-storage.json new file mode 100644 index 000000000000..0b7c36cbfd68 --- /dev/null +++ b/docs/contents/cloud-storage.json @@ -0,0 +1,13 @@ +[{ + "title": "StorageClient", + "type": "storage/storageclient" +}, { + "title": "ACL", + "type": "storage/acl" +}, { + "title": "Bucket", + "type": "storage/bucket" +}, { + "title": "StorageObject", + "type": "storage/storageobject" +}] diff --git a/docs/contents/cloud-translate.json b/docs/contents/cloud-translate.json new file mode 100644 index 000000000000..d829deaa0161 --- /dev/null +++ b/docs/contents/cloud-translate.json @@ -0,0 +1,4 @@ +[{ + "title": "TranslateClient", + "type": "translate/translateclient" +}] diff --git a/docs/contents/cloud-vision.json b/docs/contents/cloud-vision.json new file mode 100644 index 000000000000..1ffef7a6ec5f --- /dev/null +++ b/docs/contents/cloud-vision.json @@ -0,0 +1,34 @@ +[{ + "title": "VisionClient", + "type": "vision/visionclient" +}, { + "title": "Image", + "type": "vision/image" +}, { + "title": "Annotation", + "type": "vision/annotation", + "nav": [ + { + "title": "CropHint", + "type": "vision/annotation/crophint" + }, { + "title": "Document", + "type": "vision/annotation/document" + }, { + "title": "Entity", + "type": "vision/annotation/entity" + }, { + "title": "Face", + "type": "vision/annotation/face" + }, { + "title": "ImageProperties", + "type": "vision/annotation/imageproperties" + }, { + "title": "SafeSearch", + "type": "vision/annotation/safesearch" + }, { + "title": "Web", + "type": "vision/annotation/web" + } + ] +}] diff --git a/docs/contents/google-cloud.json b/docs/contents/google-cloud.json new file mode 100644 index 000000000000..ab88991df84b --- /dev/null +++ b/docs/contents/google-cloud.json @@ -0,0 +1,4 @@ +[{ + "title": "ServiceBuilder", + "type": "servicebuilder" +}] diff --git a/docs/home.html b/docs/home.html index 46eb81f7d874..82f9296f1687 100644 --- a/docs/home.html +++ b/docs/home.html @@ -22,7 +22,7 @@