diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3fcea6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +/vendor/ +composer.lock + + +.idea + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + diff --git a/README.md b/README.md index 495109a..c872a89 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,51 @@ -ContinuousPHP + + ContinuousPHP +

- Build Status - Version - Packagist + Build Status

+

ContinuousPHP© is the first and only PHP-centric PaaS to build, package, test and deploy applications in the same workflow.

# ContinuousPHP\Cli -CLI for ContinuousPHP platform. Manage project and build easily from your favorite terminal. +CLI for the ContinuousPHP platform. Manage projects and build easily from your favorite terminal. -## Installation +## Installation as Phar ( Recommended ) -With [Composer](https://getcomposer.org/), to include this library into your dependencies, you need to require [`continuousphp/cli`](https://packagist.org/packages/continuousphp/cli): +Download the latest version of continuousphpcli as a Phar: ```sh -$ composer require continuousphp/cli '~0.0' +$ curl -LSs https://continuousphp.github.io/cli/phar-installer.php | php ``` -## Usage +The command will check your PHP settings, warn you of any issues, and then download it to the current directory. +From there, you may place it anywhere you want to make it easier to access (such as `/usr/local/bin`) and chmod it to 755. +You can even rename it to just `continuousphpcli` to avoid having to type the .phar extension every time. + +## Documentation + +You can find Markdown documentation into `docs` subfolder or on web version at https://continuousphp.github.io/cli/doc +Thanks to open an issue if you see something missing in our documentation. + +## Credit + +This project was made based on Open-Source project, thanks to them! + + * [Box](https://github.com/box-project/box2) - PHAR builder + * [Symfony\Console](https://github.com/symfony/console) - PHP Console Service + * [Hoa\Console](https://github.com/hoaproject/Console) - PHP Console library ## Contributing 1. Fork it :clap: 2. Create your feature branch: `git checkout -b feat/my-new-feature` -3. Write your Unit and Functional testing +3. Write your Unit and Functional tests 4. Commit your changes: `git commit -am 'Add some feature'` 5. Push to the branch: `git push origin feat/my-new-feature` -6. Submit a pull request with the detail of your implementation +6. Submit a pull request with the details of your implementation 7. Take a drink during our review and merge :beers: diff --git a/bin/continuousphp b/bin/continuousphp new file mode 100755 index 0000000..05e96a7 --- /dev/null +++ b/bin/continuousphp @@ -0,0 +1,20 @@ +#!/usr/bin/env php +create() + ->run() +; diff --git a/box.json b/box.json new file mode 100644 index 0000000..7700554 --- /dev/null +++ b/box.json @@ -0,0 +1,30 @@ +{ + "files": [ + "constants.php" + ], + "directories": [ + "src" + ], + "finder": [ + { + "name": "*.php", + "exclude": [ + "phpunit", + "phpunit-test-case", + "Tester", + "Tests", + "Test", + "tests", + "yaml" + ], + "in": "vendor" + } + ], + "git-version": "git-version", + "replacements": { + "my-custom-place-holder": "custom-value-dev-master" + }, + "main": "bin/continuousphp", + "output": "continuousphp-@git-version@.phar", + "stub": true +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8cc0075 --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "continuousphp/cli", + "description": "The command line interface to ContinuousPHP Platform", + "type": "library", + "license": "Apache-2.0", + "authors": [ + { + "name": "Pierre Tomasina", + "email": "pierre.tomasina@continuousphp.com" + } + ], + "require": { + "continuousphp/sdk": "dev-feat/entities", + "symfony/console": "^3.3", + "hoa/console": "~3.0" + }, + "autoload": { + "psr-4": { + "Continuous\\Cli\\": "src/" + }, + "files": ["constants.php"] + }, + "autoload-dev": { + "psr-4": { + "Continuous\\Cli\\Tests\\": "tests/" + } + }, + "bin": ["bin/continuousphp"], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Pierozi/guzzle-services.git" + } + ] +} diff --git a/constants.php b/constants.php new file mode 100644 index 0000000..d9cdb0a --- /dev/null +++ b/constants.php @@ -0,0 +1,8 @@ +"continuousphpcli.phar","sha1"=>sha1_file("../'$PHAR_NAME'"),"url"=>"https://github.com/continuousphp/cli/releases/download/'$TAG'/continuousphpcli.phar","version"=>substr("'$TAG'",1)]; file_put_contents("manifest.json", json_encode($x)); print_r($x);' + +git config user.email "info@continuousphp.com" +git config user.name "${CPHP_BUILT_BY}" + +git add -A doc +git add manifest.json + +git commit -m "Update doc to tag $TAG" +git push origin gh-pages \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b043f72 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ +# What is ContinuousPHP + +ContinuousPHP is the first and only PHP-centric PaaS to build, package, test and deploy applications in the same workflow. + +The ContinuousPHP CLI is a command line interface for the ContinuousPHP Platform. + +## Installation + +We recommend using the php installer script to install the latest version +of continuousphpcli PHAR. + + $ curl -LSs https://continuousphp.github.io/cli/phar-installer.php | php + # Move the phar in your user bin directory + $ mv continuousphpcli.phar /usr/local/bin/continuousphpcli + +The command will check your PHP settings, warn you of any issues, and then download it to the current directory. +From there, you may place it anywhere you want to make it easier to access (such as `/usr/local/bin`) and chmod it to 755. +You can even rename it to just `continuousphpcli` to avoid having to type the .phar extension every time. + +## Configuration + +By default, some of the continuousphp API requests do not require to be authenticated. +But you will certainly need to authenticate for commands that require permissions, like starting or stopping a build. + +The cli implements a profile system to easily use different continuousphp accounts. + +Each profile must be configured with the continuousphp user token. You can find a personal token +on your credentials page at https://app.continuousphp.com/credentials + +Configure a new profile in interactive mode with this command: + + $ continuousphpcli configure + > Profile name [default]: myProfileName + > User Token: XXXXXXXXXX + < Profile myUserAccount saved in /home/user/.continuousphp/credentials + +If you choose `default` as the profile name, the continuousphpcli will automatically use this credential. +Otherwise, you must specify the option `--profile myProfileName` on each command. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..d72fa8c --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,2 @@ +site_name: ContinuousPHP Cli +theme: 'material' diff --git a/src/ApplicationFactory.php b/src/ApplicationFactory.php new file mode 100644 index 0000000..d0485e4 --- /dev/null +++ b/src/ApplicationFactory.php @@ -0,0 +1,50 @@ +add(new ConfigureCommand()); + $application->add(new CompanyListCommand()); + $application->add(new RepositoryListCommand()); + $application->add(new ProjectListCommand()); + $application->add(new BuildListCommand()); + $application->add(new BuildStartCommand()); + $application->add(new BuildStopCommand()); + + return $application; + } + + /** + * Return the current version of continuousphp cli. + * + * @return string + */ + public static function getVersion() + { + return constant(__NAMESPACE__ . '\\' . 'version'); + } +} \ No newline at end of file diff --git a/src/Command/Build/BuildListCommand.php b/src/Command/Build/BuildListCommand.php new file mode 100644 index 0000000..6258975 --- /dev/null +++ b/src/Command/Build/BuildListCommand.php @@ -0,0 +1,114 @@ +setName('build:list') + ->setDescription('List of builds for specific project.') + ->setHelp('This command help you to list the builds of specific project and pipeline.') + ->addArgument('provider', InputArgument::REQUIRED, 'The repository provider') + ->addArgument('repository', InputArgument::REQUIRED, 'The repository name') + ; + + $this + ->addOption( + 'ref', + 'r', + InputOption::VALUE_OPTIONAL, + 'the pipeline ref' + ) + ->addOption( + 'state', + 's', + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'the build status', + Build::STATE + ) + ->addOption( + 'noPr', + null, + InputOption::VALUE_NONE, + 'remove the PullRequest of result' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $this->showLoader($output, 'Loading builds...'); + $ref = $input->getOption('ref'); + $state = $input->getOption('state'); + + $params = [ + 'provider' => static::mapProviderToSdk($input->getArgument('provider')), + 'repository' => $input->getArgument('repository'), + 'state' => $state, + ]; + + if ($ref) { + $params['pipeline_id'] = $ref; + } + + if (true === $input->getOption('noPr')) { + $params['exclude_pull_requests'] = '1'; + } + + /** @var Collection $collection */ + $collection = $this->continuousClient->getBuilds($params); + $rows = []; + + $this->hideLoader($output); + + foreach ($collection as $id => $build) { + + $created = \DateTimeImmutable::createFromFormat('Y-m-d*H:i:sP', $build->get('created')); + + $launchUser = $build->getLaunchUser(); + $result = $build->get('result'); + $resultOutput = "$result"; + + $successActivities = array_filter($build->get('activities'), function($item) { + return true === $item['result']; + }); + + $rows[] = [ + $id, + $build->get('ref'), + $build->get('pullRequestNumber') ? $build->get('pullRequestNumber') : "-", + $build->get('state'), + $resultOutput, + $build->getDuration()->format('%H:%I:%S'), + count($successActivities) . '/' . count($build->get('activities')), + round($build->get('codeCoverage')) . "%", + $launchUser->displayName(), + $created->format('d/m/Y H:i:s'), + ]; + } + + $table = new Table($output); + $table + ->setHeaders(['ID', 'Ref', 'PR', 'State', 'Result', 'Duration', 'Activities Success', 'Code Coverage', 'Launch by', 'date']) + ->setRows($rows) + ->render() + ; + } +} \ No newline at end of file diff --git a/src/Command/Build/BuildStartCommand.php b/src/Command/Build/BuildStartCommand.php new file mode 100644 index 0000000..9ef8113 --- /dev/null +++ b/src/Command/Build/BuildStartCommand.php @@ -0,0 +1,69 @@ +setName('build:start') + ->setDescription('start a build for specific project.') + ->setHelp('This command help you to start build for specific pipeline project.') + ->addArgument('provider', InputArgument::REQUIRED, 'The repository provider') + ->addArgument('repository', InputArgument::REQUIRED, 'The repository name') + ->addArgument('ref', InputArgument::REQUIRED, 'The git reference') + ; + + $this + ->addOption( + 'pull-request', + 'pr', + InputOption::VALUE_OPTIONAL, + 'the PR id you want build' + ) + ; + + $this + ->addOption( + 'attach', + 'a', + InputOption::VALUE_NONE, + 'attach the log' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $this->showLoader($output, 'Starting builds...'); + + $params = [ + 'provider' => static::mapProviderToSdk($input->getArgument('provider')), + 'repository' => $input->getArgument('repository'), + 'ref' => $input->getArgument('ref'), + ]; + + if ($pr = $input->getOption('pull-request')) { + $params['pull_request'] = $pr; + } + + /** @var Build $build */ + $build = $this->continuousClient->startBuild($params); + + $output->writeln('Build started with ID ' . $build->get('buildId')); + } +} \ No newline at end of file diff --git a/src/Command/Build/BuildStopCommand.php b/src/Command/Build/BuildStopCommand.php new file mode 100644 index 0000000..b62695e --- /dev/null +++ b/src/Command/Build/BuildStopCommand.php @@ -0,0 +1,45 @@ +setName('build:stop') + ->setDescription('stop a build.') + ->setHelp('This command help you to stop build for specific pipeline project.') + ->addArgument('provider', InputArgument::REQUIRED, 'The repository provider') + ->addArgument('repository', InputArgument::REQUIRED, 'The repository name') + ->addArgument('build-id', InputArgument::REQUIRED, 'The build id you want to stop') + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $this->showLoader($output, 'stopping builds...'); + + $params = [ + 'provider' => static::mapProviderToSdk($input->getArgument('provider')), + 'repository' => $input->getArgument('repository'), + 'buildId' => $input->getArgument('build-id'), + ]; + + $result = $this->continuousClient->cancelBuild($params); + var_dump($result); + + $output->writeln('Build has been cancelled.'); + } +} \ No newline at end of file diff --git a/src/Command/CommandAbstract.php b/src/Command/CommandAbstract.php new file mode 100644 index 0000000..a8b7193 --- /dev/null +++ b/src/Command/CommandAbstract.php @@ -0,0 +1,120 @@ +addTokenOption(); + } + + public static function mapProviderToSdk($provider) + { + if (in_array(strtolower(trim($provider)), ['github', 'git hub'])) + { + return 'git-hub'; + } + + if (in_array(strtolower(trim($provider)), ['bb'])) + { + return 'bitbucket'; + } + + return $provider; + } + + protected function addTokenOption() + { + $this + ->addOption( + 'token', + null, + InputOption::VALUE_OPTIONAL, + 'The token of the continuousphp user', + null + ) + ->addOption( + 'profile', + null, + InputOption::VALUE_OPTIONAL, + 'The profile of the configured credentials. See route configure', + null + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $token = $input->getOption('token'); + $profile = $input->getOption('profile'); + + if (null === $token && false === ($token = getenv('CPHP_TOKEN'))) { + + $profile = empty($profile) ? 'default' : $profile; + $token = ConfigureCommand::getToken($profile); + + if (null === $token) { + $output->writeln("WARNING : ContinuousPHP Token was not found"); + } + } + + $this->continuousClient = \Continuous\Sdk\Service::factory([ + 'token' => $token + ]); + } + + protected function showLoader($output, $message = '') + { + $this->loader = new ProgressBar($output, 1); + + if ($message) { + $this->loader->setFormatDefinition('custom', ' %current%/%max% -- %message%'); + $this->loader->setFormat('custom'); + $this->loader->setMessage($message); + + $this->loader->start(); + $this->loader->advance(); + } else { + $this->loader->start(); + } + } + + protected function hideLoader($output) + { + $this->loader->finish(); + $this->loader = null; + + $output->writeln("\n"); + } +} \ No newline at end of file diff --git a/src/Command/Company/CompanyListCommand.php b/src/Command/Company/CompanyListCommand.php new file mode 100644 index 0000000..7802197 --- /dev/null +++ b/src/Command/Company/CompanyListCommand.php @@ -0,0 +1,71 @@ +setName('company:list') + ->setDescription('List Companies.') + ->setHelp('This command related to companies declared on continuous.') + ; + + $this + ->addOption( + 'filter-name', + null, + InputOption::VALUE_OPTIONAL, + 'filter apply on name of companies result' + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $filterName = $input->getOption('filter-name'); + + $collection = $this->continuousClient->getCompanies(); + $rows = []; + + foreach ($collection as $id => $company) { + $name = $company->get('name'); + + if (null !== $filterName && false === strpos(strtolower($name), $filterName)) { + continue; + } + + $rows[] = [ + $id, + $name, + $company->get('website'), + $company->get('email'), + $company->get('vat'), + $company->get('currency'), + ]; + } + + $table = new Table($output); + $table + ->setHeaders(['ID', 'Name', 'Website', 'Email', 'Vat', 'Currency']) + ->setRows($rows) + ->render() + ; + } +} \ No newline at end of file diff --git a/src/Command/ConfigureCommand.php b/src/Command/ConfigureCommand.php new file mode 100644 index 0000000..e68b8ce --- /dev/null +++ b/src/Command/ConfigureCommand.php @@ -0,0 +1,136 @@ +setName('configure') + ->setDescription('Configure cphp profile.') + ->setHelp('This command help you to create multiple profile corresponding to cphp user token. Can be found at https://app.continuousphp.com/credentials') + ; + + $this + ->addOption( + 'profile', + null, + InputOption::VALUE_OPTIONAL, + 'Profile name' + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $this->obtainProfileToken($input, $output, $profile, $token); + $path = $this->saveProfile($profile, $token); + + $output->writeln("Profile $profile saved in $path"); + } + + protected function obtainProfileToken(InputInterface $input, OutputInterface $output, & $profile, & $token) + { + $profile = $input->getOption('profile'); + $token = $input->getOption('token'); + + if (true === $input->getOption('no-interaction')) { + if (!$profile && !$token) { + $output->writeln( + "ERROR : no-interaction was specified. You must declare profile and token as option" + ); + } + + return; + } + + $helper = $this->getHelper('question'); + + if (null === $profile) { + $profile = $helper->ask( + $input, $output, + new Question('Profile name [default]: ', 'default') + ); + } + + if (null === $token) { + $token = $helper->ask( + $input, $output, + new Question('User Token: ') + ); + } + } + + protected function saveProfile($profile, $token) + { + $profiles = self::getProfiles(); + $profiles[$profile] = [ + 'token' => $token + ]; + + $path = static::getCredentialsPath(); + $pathDir = dirname($path); + + if (!file_exists($pathDir)) { + mkdir($pathDir); + } + + $handle = fopen($path, 'w+'); + + if (false === is_resource($handle)) { + throw new \Exception("Error during opening/creating credentials file at $path"); + } + + foreach ($profiles as $name => $profile) { + fwrite($handle, "[$name]\n"); + + foreach ($profile as $k => $v) { + fwrite($handle, "$k = $v\n"); + } + } + + fclose($handle); + + return $path; + } + + protected static function getCredentialsPath() + { + $home = getenv('HOME'); + + if (empty($home)) { + $home = '~'; + } + + return "{$home}/.continuousphp/credentials"; + } + + public static function getProfiles() + { + $path = static::getCredentialsPath(); + + if (false === file_exists($path)) { + return []; + } + + return parse_ini_file($path, true); + } + + public static function getToken($profile) + { + $profiles = static::getProfiles(); + + return !empty($profiles[$profile]) ? $profiles[$profile]['token'] : null; + } +} \ No newline at end of file diff --git a/src/Command/Project/ProjectListCommand.php b/src/Command/Project/ProjectListCommand.php new file mode 100644 index 0000000..05eca41 --- /dev/null +++ b/src/Command/Project/ProjectListCommand.php @@ -0,0 +1,71 @@ +setName('project:list') + ->setDescription('List of project configured.') + ->setHelp('This command help you to find the repository already configured on ContinuousPHP.') + ; + + $this + ->addOption( + 'filter-name', + null, + InputOption::VALUE_OPTIONAL, + 'filter apply on name of repositories result' + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $filterName = $input->getOption('filter-name'); + $this->showLoader($output, 'Loading projects from providers (github, bitbucket, gitlab)...'); + + /** @var Collection $collection */ + $collection = $this->continuousClient->getProjects(); + $rows = []; + + $this->hideLoader($output); + + foreach ($collection as $id => $project) { + $name = $project->get('name'); + + if (null !== $filterName && false === strpos(strtolower($name), $filterName)) { + continue; + } + + $rows[] = [ + $project->getProvider()->get('name'), + $name, + $project->get('canSeeSettings') ? 'Yes' : "No", + $project->get('canEditSettings') ? 'Yes' : "No", + $project->get('canBuild') ? 'Yes' : "No", + ]; + } + + $table = new Table($output); + $table + ->setHeaders(['Provider', 'Name', 'View settings', 'Edit settings', 'Run build']) + ->setRows($rows) + ->render() + ; + } +} \ No newline at end of file diff --git a/src/Command/Repository/RepositoryListCommand.php b/src/Command/Repository/RepositoryListCommand.php new file mode 100644 index 0000000..d4bee46 --- /dev/null +++ b/src/Command/Repository/RepositoryListCommand.php @@ -0,0 +1,73 @@ +setName('repo:list') + ->setDescription('List of repositories not yet configured.') + ->setHelp('This command help you to find the repository that you can configure on ContinuousPHP and has not yet be initialized.') + ; + + $this + ->addOption( + 'filter-name', + null, + InputOption::VALUE_OPTIONAL, + 'filter apply on name of repositories result' + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + parent::execute($input, $output); + + $filterName = $input->getOption('filter-name'); + $this->showLoader($output, 'Loading repositories from providers (github, bitbucket, gitlab)...'); + + /** @var Collection $collection */ + $collection = $this->continuousClient->getRepositories(); + $rows = []; + + $this->hideLoader($output); + + foreach ($collection as $id => $repository) { + $name = $repository->get('name'); + + if (null !== $filterName && false === strpos(strtolower($name), $filterName)) { + continue; + } + + $rows[] = [ + $repository->getProvider()->get('name'), + $repository->get('isPrivate') ? 'Yes' : "No", + $id, + $name, + $repository->get('owner'), + $repository->get('htmlUrl'), + $repository->get('description'), + ]; + } + + $table = new Table($output); + $table + ->setHeaders(['Provider', 'Private', 'ID', 'Name', 'Owner', 'Url', 'Description']) + ->setRows($rows) + ->render() + ; + } +} \ No newline at end of file