From 757209ba3773225ae31959dea778523018b83908 Mon Sep 17 00:00:00 2001 From: Jonathan Sadowski Date: Wed, 9 Feb 2022 13:43:05 -0600 Subject: [PATCH] Add github workflow to automatically enforce the code freeze. --- .github/workflows/release-code-freeze.yml | 35 ++++ .../scripts/assign-milestone-to-merged-pr.php | 68 +------ .../workflows/scripts/post-request-shared.php | 175 ++++++++++++++++++ .../workflows/scripts/release-code-freeze.php | 61 ++++++ 4 files changed, 272 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/release-code-freeze.yml create mode 100644 .github/workflows/scripts/release-code-freeze.php diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml new file mode 100644 index 00000000000..30a0e0e3058 --- /dev/null +++ b/.github/workflows/release-code-freeze.yml @@ -0,0 +1,35 @@ +name: "Enforce release code freeze" +on: + schedule: + - cron: '0 16 * * 4' # Run at 1600 UTC on Thursdays. + +jobs: + maybe-create-next-milestone-and-release-branch: + name: "Maybe create next milestone and release branch" + runs-on: ubuntu-latest + steps: + - name: "Get the action script" + run: | + scripts="post-request-shared.php release-code-freeze.php" + for script in $scripts + do + curl \ + --silent \ + --fail \ + --header 'Authorization: bearer ${{ secrets.GITHUB_TOKEN }}' \ + --header 'User-Agent: GitHub action to enforce release code freeze' \ + --header 'Accept: application/vnd.github.v3.raw' \ + --output $script \ + --location "$GITHUB_API_URL/repos/${{ github.repository }}/contents/.github/workflows/scripts/$script?ref=$GITHUB_REF" + done + env: + GITHUB_API_URL: ${{ env.GITHUB_API_URL }} + GITHUB_REF: ${{ env.GITHUB_REF }} + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + - name: "Run the script to enforce the code freeze" + run: php release-code-freeze.php + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scripts/assign-milestone-to-merged-pr.php b/.github/workflows/scripts/assign-milestone-to-merged-pr.php index 62b32c040ca..ecf31ab7bd1 100644 --- a/.github/workflows/scripts/assign-milestone-to-merged-pr.php +++ b/.github/workflows/scripts/assign-milestone-to-merged-pr.php @@ -9,73 +9,7 @@ require_once __DIR__ . '/post-request-shared.php'; -/* - * Select the milestone to be added: - * - * 1. Get the first 10 milestones sorted by creation date descending. - * (we'll never have more than 2 or 3 active milestones but let's get 10 to be sure). - * 2. Discard those not open or whose title is not a proper version number ("X.Y.Z"). - * 3. Sort descending using version_compare. - * 4. Get the oldest one that does not have a corresponding "release/X.Y" branch. - */ - -echo "Getting the list of milestones...\n"; - -$query = " - repository(owner:\"$repo_owner\", name:\"$repo_name\") { - milestones(first: 10, states: [OPEN], orderBy: {field: CREATED_AT, direction: DESC}) { - nodes { - id - title - state - } - } - } -"; -$json = do_graphql_api_request( $query ); -$milestones = $json['data']['repository']['milestones']['nodes']; -$milestones = array_map( - function( $x ) { - return 1 === preg_match( '/^\d+\.\d+\.\d+$/D', $x['title'] ) ? $x : null; - }, - $milestones -); -$milestones = array_filter( $milestones ); -usort( - $milestones, - function( $a, $b ) { - return version_compare( $b['title'], $a['title'] ); - } -); - -echo 'Latest open milestone: ' . $milestones[0]['title'] . "\n"; - -$chosen_milestone = null; -foreach ( $milestones as $milestone ) { - $milestone_title_parts = explode( '.', $milestone['title'] ); - $milestone_release_branch = 'release/' . $milestone_title_parts[0] . '.' . $milestone_title_parts[1]; - - $query = " - repository(owner:\"$repo_owner\", name:\"$repo_name\") { - ref(qualifiedName: \"refs/heads/$milestone_release_branch\") { - id - } - } - "; - $result = do_graphql_api_request( $query ); - - if ( is_null( $result['data']['repository']['ref'] ) ) { - $chosen_milestone = $milestone; - } else { - break; - } -} - -// If all the milestones have a release branch, just take the newest one. -if ( is_null( $chosen_milestone ) ) { - echo "WARNING: No milestone without release branch found, the newest one will be assigned.\n"; - $chosen_milestone = $milestones[0]; -} +$chosen_milestone = get_latest_milestone_from_api( true ); echo 'Milestone that will be assigned: ' . $chosen_milestone['title'] . "\n"; diff --git a/.github/workflows/scripts/post-request-shared.php b/.github/workflows/scripts/post-request-shared.php index 21e9219f2a6..ce2462b415d 100644 --- a/.github/workflows/scripts/post-request-shared.php +++ b/.github/workflows/scripts/post-request-shared.php @@ -19,9 +19,184 @@ $repo_name = $repo_parts[1]; $pr_id = getenv( 'PULL_REQUEST_ID' ); $github_token = getenv( 'GITHUB_TOKEN' ); +$github_api_url = getenv( 'GITHUB_API_URL' ); $graphql_api_url = getenv( 'GITHUB_GRAPHQL_URL' ); +/** + * Function to get the latest milestone. + * + * @param bool $use_latest_when_null When true, the function returns the latest milestone regardless of release branch status. + * @return string The title of the latest milestone. + */ +function get_latest_milestone_from_api( $use_latest_when_null = false ) { + global $repo_owner, $repo_name; + + echo "Getting the list of milestones...\n"; + + $query = " + repository(owner:\"$repo_owner\", name:\"$repo_name\") { + milestones(first: 10, states: [OPEN], orderBy: {field: CREATED_AT, direction: DESC}) { + nodes { + id + title + state + } + } + } + "; + $json = do_graphql_api_request( $query ); + $milestones = $json['data']['repository']['milestones']['nodes']; + $milestones = array_map( + function( $x ) { + return 1 === preg_match( '/^\d+\.\d+\.\d+$/D', $x['title'] ) ? $x : null; + }, + $milestones + ); + $milestones = array_filter( $milestones ); + usort( + $milestones, + function( $a, $b ) { + return version_compare( $b['title'], $a['title'] ); + } + ); + + echo 'Latest open milestone: ' . $milestones[0]['title'] . "\n"; + + $chosen_milestone = null; + foreach ( $milestones as $milestone ) { + $milestone_title_parts = explode( '.', $milestone['title'] ); + $milestone_release_branch = 'release/' . $milestone_title_parts[0] . '.' . $milestone_title_parts[1]; + + $query = " + repository(owner:\"$repo_owner\", name:\"$repo_name\") { + ref(qualifiedName: \"refs/heads/$milestone_release_branch\") { + id + } + } + "; + $result = do_graphql_api_request( $query ); + + if ( is_null( $result['data']['repository']['ref'] ) ) { + $chosen_milestone = $milestone; + } else { + break; + } + } + + // If all the milestones have a release branch, just take the newest one. + if ( $use_latest_when_null && is_null( $chosen_milestone ) ) { + echo "WARNING: No milestone without release branch found, the newest one will be assigned.\n"; + $chosen_milestone = $milestones[0]; + } + + return $chosen_milestone; +} + +/** + * Function to retreive the sha1 reference for a given branch name. + * + * @param string $branch The name of the branch. + * @return string Returns the name of the branch, or a falsey value on error. + */ +function get_ref_from_branch( $branch ) { + global $repo_owner, $repo_name; + $query = " + repository(owner:\"$repo_owner\", name:\"$repo_name\") { + ref(qualifiedName: \"refs/heads/{$branch}\") { + target { + ... on Commit { + history(first: 1) { + edges{ node{ oid } } + } + } + } + } + } + "; + $result = do_graphql_api_request( $query ); + // Warnings suppressed here because traversing this level of arrays with isset / is_array checks would be messy. + return @$result['data']['repository']['ref']['target']['history']['edges'][0]['node']['oid']; +} + +/** + * Function to create milestone using the GitHub REST API. + * + * @param string $title The title of the milestone to be created. + * @return bool True on success, False otherwise. + */ +function create_github_milestone( $title ) { + global $repo_owner, $repo_name; + + $result = do_github_api_post_request( "/repos/{$repo_owner}/{$repo_name}/milestones", array( + 'title' => $title, + ) ); + return is_array( $result ) && $result['title'] === $title; +} + +/** + * Function to create branch using the GitHub REST API. + * + * @param string $branch The branch to be created. + * @param string $sha The sha1 reference for the branch. + * @return bool True on success, False otherwise. + */ +function create_github_branch( $branch, $sha ) { + global $repo_owner, $repo_name; + + $ref = "refs/heads/{$branch}"; + $result = do_github_api_post_request( "/repos/{$repo_owner}/{$repo_name}/git/refs", array( + 'ref' => $ref, + 'sha' => $sha, + ) ); + return is_array( $result ) && $result['ref'] === $ref; +} + +/** + * Function to create branch using the GitHub REST API from an existing branch. + * + * @param string $source The branch from which to create. + * @param string $target The branch to be created. + * @return bool True on success, False otherwise. + */ +function create_github_branch_from_branch( $source, $target ) { + $ref = get_ref_from_branch( $source ); + if ( ! $ref ) { + return false; + } + return create_github_branch( $target, $ref ); +} + +/** + * Function to do a GitHub API POST Request. + * + * @param array $request_url + * @param array $body The body of the request to be json encoded. + * @return mixed The json-decoded response if a response is received, 'false' (or whatever file_get_contents returns) otherwise. + */ +function do_github_api_post_request( $request_path, $body ) { + global $github_token, $github_api_url; + + $context = stream_context_create( + array( + 'http' => array( + 'method' => 'POST', + 'header' => array( + 'Accept: application/vnd.github.v3+json', + 'Content-Type: application/json', + 'User-Agent: GitHub Actions for creation of milestones', + 'Authorization: bearer ' . $github_token, + ), + 'content' => json_encode( $body ), + ), + ) + ); + + $full_request_url = rtrim( $github_api_url, '/' ) . '/' . ltrim( $request_path, '/' ); + $result = file_get_contents( $full_request_url, false, $context ); + return is_string( $result ) ? json_decode( $result, true ) : $result; +} + /** * Function to query the GitHub GraphQL API. * diff --git a/.github/workflows/scripts/release-code-freeze.php b/.github/workflows/scripts/release-code-freeze.php new file mode 100644 index 00000000000..f5d27529783 --- /dev/null +++ b/.github/workflows/scripts/release-code-freeze.php @@ -0,0 +1,61 @@ + 14 ) { + echo "Info: Today is not the Thursday of the code freeze.\n"; + return; +} + +$latest_milestone = get_latest_milestone_from_api(); + +if ( is_null( $latest_milestone ) ) { + echo "*** Error: Unable to get latest milestone\n"; + return; +} + +$version_parts = explode( '.', $latest_milestone['title'], 3 ); +$latest_major_minor = "{$version_parts[0]}.{$version_parts[1]}"; + +// Because we go from 5.9 to 6.0, we can get the next major_minor by adding 0.1 and formatting appropriately. +$latest_float = (float) $latest_major_minor; +$next_major_minor = number_format( $latest_float + 0.1, 1 ); + +// We use those values to get the release branch and next milestones that we need to create. +$release_branch_to_create = "release/{$latest_major_minor}"; +$milestone_to_create = "{$next_major_minor}.0"; + +if ( getenv( 'DRY_RUN' ) ) { + echo "DRY RUN: Skipping actual creation of release branch and milestone...\n"; + echo "Release Branch: {$release_branch_to_create}\n"; + echo "Milestone: {$milestone_to_create}\n"; + return; +} + +if ( create_github_milestone( $milestone_to_create ) ) { + echo "Created milestone {$milestone_to_create}.\n"; +} else { + echo "*** Error: Unable to create {$milestone_to_create} milestone.\n"; +} + +if ( create_github_branch_from_branch( 'trunk', $release_branch_to_create ) ) { + echo "Created branch {$release_branch_to_create}.\n"; +} else { + echo "*** Error: Unable to create {$release_branch_to_create}.\n"; +} \ No newline at end of file