Skip to content

Commit

Permalink
Merge pull request #53 from creative-commoners/pulls/main/branch-prot…
Browse files Browse the repository at this point in the history
…ection

NEW Update rulesets
  • Loading branch information
GuySartorelli authored May 29, 2024
2 parents dfc76cd + 9ecdfdf commit 4efdcbe
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 1 deletion.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,22 @@ MS_GITHUB_TOKEN=abc123 php run.php labels --dry-run --only=silverstripe-config,s
| --exclude=[modules] | Exclude the specified modules (without account prefix) separated by commas e.g. `silverstripe-mfa,silverstripe-totp` |
| --dry-run | Do not update labels in GitHub, output to terminal only |
| --no-delete | Do not delete `_data` directory before running |

## Usage - Standardising GitHub repository branch and tag rulesets

```bash
MS_GITHUB_TOKEN=<token> php run.php rulesets <options>
```

**Example usage:**
```bash
MS_GITHUB_TOKEN=abc123 php run.php rulesets --dry-run --only=silverstripe-config,silverstripe-assets
```

### Command line options:

| Flag | Description |
| ---- | ------------|
| --only=[modules] | Only include the specified modules (without account prefix) separated by commas e.g. `silverstripe-config,silverstripe-assets` |
| --exclude=[modules] | Exclude the specified modules (without account prefix) separated by commas e.g. `silverstripe-mfa,silverstripe-totp` |
| --dry-run | Do not update rulesets in GitHub, output to terminal only |
41 changes: 40 additions & 1 deletion funcs_utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ function github_api($url, $data = [], $httpMethod = '')
{
// silverstripe-themes has a kind of weird redirect only for api requests
$url = str_replace('/silverstripe-themes/silverstripe-simple', '/silverstripe/silverstripe-simple', $url);
info("Making curl request to $url");
$method = $httpMethod ? strtoupper($httpMethod) : 'GET';
info("Making $method curl request to $url");
$token = github_token();
$jsonStr = empty($data) ? '' : json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ch = curl_init($url);
Expand Down Expand Up @@ -244,6 +245,44 @@ function output_repos_with_labels_updated()
$io->writeln('');
}

/**
* Outputs a list of repos that that had rulesets updated
* If there was an error with a run (probably a secondary rate limit), this can be
* copy pasted into the --exclude option for the next run to continue from where you left off
*/
function output_repos_with_rulesets_created_or_updated()
{
if (running_unit_tests()) {
return;
}
global $REPOS_WITH_RULESETS_UPDATED;
$io = io();
$io->writeln('');
$io->writeln('Repos with rulesets created/updated (add to --exclude if you need to re-run):');
$io->writeln(implode(',', $REPOS_WITH_RULESETS_UPDATED));
$io->writeln('');
}

function create_ruleset($type, $additionalBranchConditions = [])
{
$ruleset = file_get_contents("rulesets/$type-ruleset.json");
if (!$ruleset) {
error("Could not read ruleset for $type");
}
$json = json_decode($ruleset, true);
if ($type == 'branch') {
$json['name'] = BRANCH_RULESET_NAME;
} elseif ($type === 'tag') {
$json['name'] = TAG_RULESET_NAME;
} else {
error("Invalid ruleset type: $type");
}
foreach ($additionalBranchConditions as $value) {
$json['conditions']['ref_name']['include'][] = $value;
}
return $json;
}

/**
* Works out which branch in a module to checkout before running scripts on it
*
Expand Down
44 changes: 44 additions & 0 deletions rulesets/branch-ruleset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "<RULESET_NAME>",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"exclude": [],
"include": [
"refs/heads/[0-9]*"
]
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "creation"
},
{
"type": "update"
},
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 2,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": true,
"required_review_thread_resolution": false
}
}
],
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "always"
}
]
}
34 changes: 34 additions & 0 deletions rulesets/tag-ruleset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "<RULESET_NAME>",
"target": "tag",
"enforcement": "active",
"conditions": {
"ref_name": {
"exclude": [],
"include": [
"~ALL"
]
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "creation"
},
{
"type": "update"
}
],
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "always"
}
]
}
120 changes: 120 additions & 0 deletions rulesets_command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

use SilverStripe\SupportedModules\MetaData;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;

$rulesetsCommand = function(InputInterface $input, OutputInterface $output): int {
// This is the code that is executed when running the 'rulesets' command

// Variables
global $OUT, $REPOS_WITH_RULESETS_UPDATED;
$OUT = $output;

// Validate system is ready
validate_system();

// Modules
$modules = [];
$modulesCurrentMajor = filtered_modules(MetaData::HIGHEST_STABLE_CMS_MAJOR, $input);
$modulesPreviousMajor = filtered_modules(MetaData::HIGHEST_STABLE_CMS_MAJOR - 1, $input);
foreach ([$modulesCurrentMajor, $modulesPreviousMajor] as $modulesList) {
foreach ($modulesList as $module) {
// Important! Only include modules on the "silverstripe" account
if ($module['account'] !== 'silverstripe') {
continue;
}
$modules[$module['ghrepo']] = $module;
}
}

// Update rulesets
foreach ($modules as $module) {
$account = $module['account'];
$repo = $module['repo'];

// Fetch existing rulesets
// https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#get-all-repository-rulesets
$rulesets = github_api("https://api.github.com/repos/$account/$repo/rulesets");
$branchRulesetID = null;
$tagRulesetID = null;
foreach ($rulesets as $ruleset) {
$id = $ruleset['id'];
$name = $ruleset['name'];
if ($name === BRANCH_RULESET_NAME) {
$branchRulesetID = $id;
}
if ($name === TAG_RULESET_NAME) {
$tagRulesetID = $id;
}
}

// Get any additional branches to add
// Assumption is that if the default branch is main/master, then the repo uses
// a non-numeric style branching system (e.g. main, master) and that needs to be protected
// [0-9]* branch protection will still be applied, on the chance that the repo is converted
// to uses a numeric style branch system in the future and we would want branch protection
// to start immediately on the new branches
$additionalBranchConditions = [];
// https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository
$defaultBranch = github_api("https://api.github.com/repos/$account/$repo")['default_branch'];
if (in_array($defaultBranch, ['main', 'master'])) {
$additionalBranchConditions[] = "refs/heads/$defaultBranch";
}

// Create rulesets
// Note: This will read from the "rulesets" directory
// In each of those json rulesets there is "bypass_actors"."actor_id" = 5
// This translates to the "Repository admin" role
// It has been confirmed that the github-action user is able to bypass the ruleset as
// it has the "Organisation admin" role which is one level above the "Repository admin" role
$branchRuleset = create_ruleset('branch', $additionalBranchConditions);
$tagRuleset = create_ruleset('tag');

$apiCalls = [];

// Create new rulesets
if (is_null($branchRulesetID)) {
// https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#create-a-repository-ruleset
$url = "https://api.github.com/repos/$account/$repo/rulesets";
$apiCalls[] = [$url, $branchRuleset, 'POST'];
}
if (is_null($tagRulesetID)) {
// https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#create-a-repository-ruleset
$url = "https://api.github.com/repos/$account/$repo/rulesets";
$apiCalls[] = [$url, $tagRuleset, 'POST'];
}

// Update existing rulesets
// Don't bother to check if the ruleset is already correct
// This is a very quick update so no need to optimise this
if (!is_null($branchRulesetID)) {
// https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#update-a-repository-ruleset
$url = "https://api.github.com/repos/$account/$repo/rulesets/$branchRulesetID";
$apiCalls[] = [$url, $branchRuleset, 'PUT'];
}
if (!is_null($tagRulesetID)) {
// https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#update-a-repository-ruleset
$url = "https://api.github.com/repos/$account/$repo/rulesets/$tagRulesetID";
$apiCalls[] = [$url, $tagRuleset, 'PUT'];
}

if ($input->getOption('dry-run')) {
info('Not updating rulesets on GitHub because --dry-run option is set');
info('There API calls would have been made:');
foreach ($apiCalls as $apiCall) {
info($apiCall[2] . ' ' . $apiCall[0]);
}
} else {
foreach ($apiCalls as $apiCall) {
github_api($apiCall[0], $apiCall[1], $apiCall[2]);
}
}

$REPOS_WITH_RULESETS_UPDATED[] = $repo;
}

output_repos_with_rulesets_created_or_updated();
return Command::SUCCESS;
};
11 changes: 11 additions & 0 deletions run.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
include 'funcs_utils.php';
include 'update_command.php';
include 'labels_command.php';
include 'rulesets_command.php';

use SilverStripe\SupportedModules\MetaData;
use Symfony\Component\Console\Application;
Expand All @@ -19,13 +20,16 @@
const TOOL_URL = 'https://github.com/silverstripe/module-standardiser';
const PR_TITLE = 'MNT Run module-standardiser';
const PR_DESCRIPTION = 'This pull-request was created automatically by [module-standardiser](' . TOOL_URL . ')';
const BRANCH_RULESET_NAME = 'Silverstripe CMS branch ruleset';
const TAG_RULESET_NAME = 'Silverstripe CMS tag ruleset';

// global variables
$MODULE_DIR = '';
$GITHUB_REF = '';
$PRS_CREATED = [];
$REPOS_WITH_PRS_CREATED = [];
$REPOS_WITH_LABELS_UPDATED = [];
$REPOS_WITH_RULESETS_UPDATED = [];
$OUT = null;

// options
Expand Down Expand Up @@ -102,6 +106,13 @@
->addOption(...$optionNoDelete)
->setCode($labelsCommand);

$app->register('rulesets')
->setDescription('Script to set rulesets on all repos only on the silverstripe account')
->addOption(...$optionOnly)
->addOption(...$optionExclude)
->addOption(...$optionDryRun)
->setCode($rulesetsCommand);

try {
$app->run();
} catch (Error|Exception $e) {
Expand Down

0 comments on commit 4efdcbe

Please sign in to comment.