src/Utils/Actions/RunCsFixerCommand.php (155 lines of code) (raw):

<?php /** * Copyright 2024 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Google\Cloud\Utils\Actions; use GuzzleHttp\Client; 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 Symfony\Component\Yaml\Yaml; /** * CLI command for running the CS Fixer shared workflow. */ class RunCsFixerCommand extends Command { private Client $client; protected function configure() { $this ->setName('cs-fixer') ->setDescription('Execute a command with the deployed image') ->addArgument( 'repo', InputArgument::REQUIRED, 'The name of the repo to run the CS fixer for' ) ->addOption( 'workflow-file', '', InputOption::VALUE_REQUIRED, 'name of the github workflow file which contains the configuration', null ) ->addOption( 'ref', '', InputOption::VALUE_REQUIRED, 'The branch of the repo run the CS fixer for', 'main' ) ->addOption( 'flags', '', InputOption::VALUE_REQUIRED, 'The flags to pass down to the CS fixer', '--dry-run --diff' ); } protected function execute(InputInterface $input, OutputInterface $output) { $repo = $input->getArgument('repo'); if (false === strpos($repo, '/')) { throw new \Exception('Invalid repo name. Use the format: owner/repo'); } $this->client = new Client(['http_errors' => false]); $ref = $input->getOption('ref'); $job = ($workflowFile = $input->getOption('workflow-file')) ? $this->getJobFromWorkflowFile($repo, $ref, $workflowFile) : $this->determineWorkflowFile($repo, $ref); if (!$job) { throw new \Exception('No job found for php-tools/code-standards.yaml found in the workflow file(s)'); } $output->writeln(sprintf('Using workflow job "%s" in "%s"', $job['name'], $job['file'])); // get the default config $defaultWorkflow = Yaml::parse(file_get_contents(__DIR__ . '/../../../.github/workflows/code-standards.yml')); $defaults = []; foreach ($defaultWorkflow['on']['workflow_call']['inputs'] as $name => $inputOptions) { $defaults[$name] = $inputOptions['default'] ?? ''; } $options = array_merge($defaults, $job['with'] ?? []); if (str_starts_with($options['config'], 'GoogleCloudPlatform/php-tools/')) { // use local file $options['config'] = str_replace( 'GoogleCloudPlatform/php-tools/', __DIR__ . '/../../../', $options['config'] ); // strip branch (we'll ignore it in favor of the current branch) if (false !== $i = strpos($options['config'], '@')) { $options['config'] = substr($options['config'], 0, $i); } if (!file_exists($options['config'])) { throw new \Exception('config file not found: ' . realpath($options['config'])); } } // go through config options and set env vars accordingly $rules = json_encode(array_merge( json_decode($options['rules'], true), json_decode($options['add-rules'], true) )); $excludePatterns = str_replace(["\n", ' '], '', $options['exclude-patterns']); // use config path only if EXCLUDE_PATTERN is empty if ($options['config']) { // set environment variables so they're available in the CONFIG file $env = sprintf( 'CONFIG_PATH=%s RULES=$\'%s\' EXCLUDE_PATTERNS=$\'%s\'', $options['path'], $rules, $excludePatterns, ); // Run command using the --config flag $cmd = sprintf( '%s ~/.composer/vendor/bin/php-cs-fixer fix --config=%s %s', $env, $options['config'], $input->getOption('flags') ); } else { // Run command using the --rules flag $cmd = sprintf( '~/.composer/vendor/bin/php-cs-fixer fix %s --rules=$\'%s\' %s', $options['path'], $rules, $input->getOption('flags') ); } $output->writeln('Executing the following command: '); $output->writeln(''); $output->writeln("<info>\t" . $cmd . '</>'); $output->writeln(''); // @TODO use Symfony process component to run this passthru($cmd, $resultCode); return $resultCode; } private function determineWorkflowFile(string $repo, string $ref): ?array { $url = sprintf( 'https://api.github.com/repos/%s/contents/.github/workflows', $repo ); $response = $this->client->request('GET', $url); if ($response->getStatusCode() === 404) { throw new \Exception('Failed to determine the workflow file, maybe the repo doesn\'t exist?'); } foreach (json_decode($response->getBody(), true) as $workflow) { if ($job = $this->getJobFromWorkflowFile($repo, $ref, $workflow['name'])) { return $job; } } return null; } private function getJobFromWorkflowFile(string $repo, string $ref, string $workflowFile): ?array { $url = sprintf( 'https://raw.githubusercontent.com/%s/%s/.github/workflows/%s', $repo, $ref, $workflowFile, ); $response = $this->client->request('GET', $url); if ($response->getStatusCode() === 404) { throw new \Exception(sprintf( 'Failed to fetch the workflow file at "%s", maybe it doesn\'t exist? ' . 'Try supplying the "--workflow-file" option.', $url )); } $workflow = Yaml::parse($response->getBody()); foreach ($workflow['jobs'] as $id => $job) { if (str_contains($job['uses'] ?? '', '.github/workflows/code-standards.yml')) { $job['name'] ??= $id; $job['file'] = $workflowFile; return $job; } } return null; } }