diff --git a/.github/workflows/release-checks.yaml b/.github/workflows/release-checks.yaml index 35bfddab056..7f92130c8d9 100644 --- a/.github/workflows/release-checks.yaml +++ b/.github/workflows/release-checks.yaml @@ -133,3 +133,23 @@ jobs: if: github.event.pull_request.user.login == 'release-please[bot]' with: next-release-label-check: true + + # Ensure all repos are in compliance + repo-compliance-check: + name: Repo Compliance Check + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'release-please[bot]' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + - name: "Install dependencies" + run: composer install -d dev + - name: "Check repo compliance" + env: + GH_TOKEN: ${{ secrets.SPLIT_TOKEN }} + run: ./dev/google-cloud repo:compliance --format=ci -t $GH_TOKEN diff --git a/dev/src/Command/ComponentExecuteCommand.php b/dev/src/Command/ComponentExecuteCommand.php index 0767022b558..f4764a556fb 100644 --- a/dev/src/Command/ComponentExecuteCommand.php +++ b/dev/src/Command/ComponentExecuteCommand.php @@ -41,6 +41,16 @@ protected function configure() { $this->setName('component:execute') ->setDescription('Execute a command for each component') + ->setHelp(<<getPath() . '/SECURITY.md');" + +Execute a PHP file: + + ./dev/google-cloud component:execute copy_file.php + +EOF) ->addArgument('code', InputArgument::REQUIRED, 'Path to a file or PHP code to execute') ; } diff --git a/dev/src/Command/RepoComplianceCommand.php b/dev/src/Command/RepoComplianceCommand.php index a38e0154507..5feac19ad43 100644 --- a/dev/src/Command/RepoComplianceCommand.php +++ b/dev/src/Command/RepoComplianceCommand.php @@ -29,6 +29,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use GuzzleHttp\Client; +use InvalidArgumentException; /** * List repo details @@ -46,8 +47,7 @@ protected function configure() ->setDescription('ensure all github repositories meet compliance') ->addOption('component', 'c', InputOption::VALUE_REQUIRED, 'If specified, display repo info for this component only', '') ->addOption('token', 't', InputOption::VALUE_REQUIRED, 'Github token to use for authentication', '') - ->addOption('page', 'p', InputOption::VALUE_REQUIRED, 'page to start from', '1') - ->addOption('results-per-page', 'r', InputOption::VALUE_REQUIRED, 'results to display per page (0 for all)', '10') + ->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'can be "ci" or "table"', 'table') ->addOption('new-packagist-token', '', InputOption::VALUE_REQUIRED, 'update the packagist token') ; } @@ -59,54 +59,44 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->github = new GitHub(new RunShell(), $http, $input->getOption('token'), $output); $this->packagist = new Packagist($http, self::PACKAGIST_USERNAME, $input->getOption('new-packagist-token') ?? ''); - $nextPageQuestion = new ConfirmationQuestion('Next Page (enter), "n" to quit: ', true); - $table = (new Table($output))->setHeaders([ - 'name' => 'Name', - 'repo_config' => 'Repo Config', - 'packagist_config' => 'Packagist Config', - 'teams' => 'Teams', - 'compliant' => 'Compliant?' - ]); - if ($componentName = $input->getOption('component')) { - $table->setVertical(); + $format = $input->getOption('format'); + if (!in_array($format, ['ci', 'table'])) { + throw new InvalidArgumentException('Invalid format "' . $format . '", must be "table" or "ci"'); } - $page = (int) $input->getOption('page'); - $resultsPerPage = (int) $input->getOption('results-per-page'); + + $table = (new Table($output)); + $table->setColumnWidths([55, 20, 20, 25, 50]); + $table->setStyle('compact'); + $headers = $format == 'ci' ? ['Name', 'Compliance'] : [ + 'Name', + 'Repo Config', + 'Packagist Config', + 'Teams', + 'Compliance' + ]; + (clone $table)->setHeaders($headers)->render(); + + $componentName = $input->getOption('component'); $components = $componentName ? [new Component($componentName)] : Component::getComponents(); - if (!$input->getOption('token')) { - $output->writeln('No token provided - please provide token to update compliance'); - } + $isCompliant = true; foreach ($components as $i => $component) { - if ($i < (($page-1) * $resultsPerPage)) { - continue; - } - if (0 !== $resultsPerPage && $i >= ($page * $resultsPerPage)) { - $table->render(); - if (!$this->getHelper('question')->ask($input, $output, $nextPageQuestion)) { - return 0; - } - $table->setRows([]); - $page++; - } $details = $this->getRepoDetails($component); - $compliance = [ - 'settings' => true, - 'packagist' => true, - 'teams' => true, - ]; + $settingsCheck = true; + $packagistCheck = true; + $teamCheck = true; $refreshDetails = false; if (!$this->checkSettingsCompliance($details)) { - $compliance['settings'] = false; + $settingsCheck = false; $refreshDetails |= $this->askFixSettingsCompliance($input, $output, $details); } if (!$this->checkPackagistCompliance($details)) { - $compliance['packagist'] = false; + $packagistCheck = false; $refreshDetails |= $this->askFixPackagistCompliance($input, $output, $component->getRepoName()); } if (!$this->checkTeamCompliance($details)) { - $compliance['teams'] = $this->github->token ? false : null; + $teamCheck = $this->github->token ? false : null; $refreshDetails |= $this->askFixTeamCompliance($input, $output, $component->getRepoName()); } if ($refreshDetails) { @@ -123,31 +113,23 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln(sprintf('%s: Packagist webhook token updated.', $repoName)); } } - $isCompliant = true; - $details['compliant'] = 'REPO IS COMPLIANT'; - foreach ($compliance as $key => $val) { - if ($val === false) { - $isCompliant = false; - $details['compliant'] = 'NOT COMPLIANT'; - } elseif ($isCompliant && $val === null) { - $details['compliant'] = '??? (token required)'; - $isCompliant = null; - } - } - if (!$isCompliant) { - $details['compliant'] .= PHP_EOL . implode("\n", array_map( - fn ($k, $v) => $k . ': ' . (is_null($v) ? '???' : var_export($v, true)), - array_keys($compliance), - array_values($compliance) - )); + $emoji = fn (?bool $check) => match ($check) { null => '❓', true => '✅', false => '❌'}; + $details['compliant'] = implode("\n", [ + sprintf('%s Issues, Projects, Wiki, Pages, and Discussion are disabled', $emoji($settingsCheck)), + sprintf('%s Packagist maintainer is "google-cloud"', $emoji($packagistCheck)), + sprintf('%s Github teams permissions are configured correctly', $emoji($teamCheck)), + '', + ]); + + $isCompliant = $isCompliant && $settingsCheck && $packagistCheck && $teamCheck; + if ($format == 'ci') { + unset($details['repo_config'], $details['packagist_config'], $details['teams']); } - - $table->addRow($details); + (clone $table)->addRow($details)->render(); } - $table->render(); - return 0; + return $isCompliant ? Command::SUCCESS : Command::FAILURE; } private function checkSettingsCompliance(array $details) @@ -169,8 +151,8 @@ private function checkTeamCompliance(array $details) private function askFixSettingsCompliance(InputInterface $input, OutputInterface $output, array $details) { - if (!$this->github->token) { - // without a token, don't ask to fix compliance + if (!$this->github->token || $input->getOption('format') == 'ci') { + // without a token, or in CI mode, don't ask to fix compliance return false; } $explodedConfig = array_map(fn ($line) => explode(': ', $line), explode("\n", $details['repo_config'])); @@ -204,8 +186,8 @@ private function checkPackagistCompliance(array $details) private function askFixPackagistCompliance(InputInterface $input, OutputInterface $output, array $details) { - if (!$this->github->token) { - // without a token, don't ask to fix compliance + if (!$this->github->token || $input->getOption('format') == 'ci') { + // without a token, or in CI mode, don't ask to fix compliance return false; } throw new \Exception('not implemented'); @@ -213,8 +195,8 @@ private function askFixPackagistCompliance(InputInterface $input, OutputInterfac private function askFixTeamCompliance(InputInterface $input, OutputInterface $output, string $repoName) { - if (!$this->github->token) { - // without a token, don't ask to fix compliance + if (!$this->github->token || $input->getOption('format') == 'ci') { + // without a token, or in CI mode, don't ask to fix compliance return false; } $question = new ConfirmationQuestion(sprintf(