From 0065fe649fa0d1e90d618a650e5b27cb1e89bca6 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Thu, 31 Oct 2024 13:52:15 +0000 Subject: [PATCH] New release workflow triggered by the merge of a PR changing IS_DEVELOPMENT_BUILD to false This is more streamlined than the previous approach, and works better for a world where 1 person isn't doing all the work. Now, the flow is simpler: - Do changes (e.g. protocol update), changelog & set IS_DEVELOPMENT_BUILD to false all in a single PR, which can be squash-merged if desired - Once the PR is merged, a draft release will be prepared - RestrictedActions will automatically set IS_DEVELOPMENT_BUILD back to true and bump the version - Tag will be created when the release is published Previously, multiple PRs might be needed, and the PR containing the release changelog couldn't be squash-merged. Manual intervention was also required to create a tag and prepare a release. This PR also includes new CI checks to check for basic errors like forgotten changelog files to ensure changelog links work correctly. Note: Only PRs from PMMP Team members with **write** access to the repository can trigger release generation. Random people cannot trigger release generation by sending PRs. --- .github/workflows/draft-release-from-pr.yml | 65 +++++++++++ .github/workflows/draft-release-from-tag.yml | 13 +++ .github/workflows/draft-release-pr-check.yml | 111 +++++++++++++++++++ .github/workflows/draft-release.yml | 31 ++++-- build/dump-version-info.php | 86 ++++++++++++++ build/make-release.php | 38 ++++--- phpstan.neon.dist | 1 + 7 files changed, 321 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/draft-release-from-pr.yml create mode 100644 .github/workflows/draft-release-from-tag.yml create mode 100644 .github/workflows/draft-release-pr-check.yml create mode 100644 build/dump-version-info.php diff --git a/.github/workflows/draft-release-from-pr.yml b/.github/workflows/draft-release-from-pr.yml new file mode 100644 index 000000000..3936a2649 --- /dev/null +++ b/.github/workflows/draft-release-from-pr.yml @@ -0,0 +1,65 @@ +name: Draft release from PR + +on: + #presume that pull_request_target is safe at this point, since the PR was approved and merged + #we need write access to prepare the release & create comments + pull_request_target: + types: + - closed + branches: + - stable + - minor-next + - major-next + - "legacy/*" + paths: + - "src/VersionInfo.php" + +jobs: + check: + name: Check release + uses: ./.github/workflows/draft-release-pr-check.yml + + draft: + name: Create GitHub draft release + needs: [check] + if: needs.check.outputs.valid == 'true' + + uses: ./.github/workflows/draft-release.yml + + post-draft-url-comment: + name: Post draft release URL as comment + needs: [draft] + + runs-on: ubuntu-20.04 + + steps: + - name: Post draft release URL on PR + uses: thollander/actions-comment-pull-request@v2 + with: + message: "[Draft release ${{ needs.draft.outputs.version }}](${{ needs.draft.outputs.draft-url }}) has been created for commit ${{ github.sha }}. Please review and publish it." + + trigger-post-release-workflow: + name: Trigger post-release RestrictedActions workflow + # Not sure if needs is actually needed here + needs: [check] + if: needs.check.outputs.valid == 'true' + + runs-on: ubuntu-20.04 + + steps: + - name: Generate access token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }} + private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }} + owner: ${{ github.repository_owner }} + repositories: RestrictedActions + + - name: Dispatch post-release restricted action + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ steps.generate-token.outputs.token }} + repository: ${{ github.repository_owner }}/RestrictedActions + event-type: pocketmine_mp_post_release + client-payload: '{"branch": "${{ github.ref }}"}' diff --git a/.github/workflows/draft-release-from-tag.yml b/.github/workflows/draft-release-from-tag.yml new file mode 100644 index 000000000..f7a5df544 --- /dev/null +++ b/.github/workflows/draft-release-from-tag.yml @@ -0,0 +1,13 @@ +#Allows creating a release by pushing a tag +#This might be useful for retroactive releases +name: Draft release from git tag + +on: + push: + tags: "*" + +jobs: + draft: + name: Create GitHub draft release + if: "startsWith(github.event.head_commit.message, 'Release ')" + uses: ./.github/workflows/draft-release.yml diff --git a/.github/workflows/draft-release-pr-check.yml b/.github/workflows/draft-release-pr-check.yml new file mode 100644 index 000000000..4c8d0f685 --- /dev/null +++ b/.github/workflows/draft-release-pr-check.yml @@ -0,0 +1,111 @@ +name: Release PR checks + +on: + #do checks on every PR update + pull_request: + branches: + - stable + - minor-next + - major-next + - "legacy/*" + paths: + - "src/VersionInfo.php" + + #allow this workflow to be invoked on PR merge, prior to creating the release + workflow_call: + outputs: + valid: + description: Whether this commit is valid for release + value: ${{ jobs.check-intent.outputs.valid && jobs.check-validity.result == 'success' }} + +permissions: + contents: read #for user access check + +jobs: + check-intent: + name: Check release trigger + runs-on: ubuntu-20.04 + + outputs: + valid: ${{ steps.validate.outputs.DEV_BUILD == 'false' }} + + steps: + - uses: actions/checkout@v4 + + - name: Check IS_DEVELOPMENT_BUILD flag + id: validate + run: | + echo DEV_BUILD=$(sed -n "s/^\s*public const IS_DEVELOPMENT_BUILD = \(true\|false\);$/\1/p" src/VersionInfo.php) >> $GITHUB_OUTPUT + + check-validity: + name: Validate release info + needs: [check-intent] + #don't do these checks if this isn't a release - we don't want to generate unnecessary failed statuses + if: needs.check-intent.outputs.valid == 'true' + + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@2.31.1 + with: + php-version: 8.2 + + - name: Restore Composer package cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/composer/files + ~/.cache/composer/vcs + key: "composer-v2-cache-${{ hashFiles('./composer.lock') }}" + restore-keys: | + composer-v2-cache- + + - name: Install Composer dependencies + run: composer install --no-dev --prefer-dist --no-interaction --ignore-platform-reqs + + - name: Check author permissions + id: check-permission + uses: actions-cool/check-user-permission@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + require: write + username: ${{ github.event.pull_request.user.login }} + #technically this would be fine for dependabot but generally bots don't count as team members + check-bot: true + + - name: Abort if user permissions are insufficient + #user doesn't have permission or is a bot + if: steps.check-permission.outputs.require-result != 'true' || steps.check-permission.outputs.check-result != 'false' + run: | + echo "::error::This user is not authorized to trigger releases" + exit 1 + + - name: Check changelog file is present + id: file-presence + run: | + CHANGELOG_FILE="changelogs/$(php build/dump-version-info.php changelog_file_name)" + if [ ! -f "${{ github.workspace }}/$CHANGELOG_FILE" ]; then + echo "::error::$CHANGELOG_FILE does not exist" + exit 1 + fi + echo FILE="$CHANGELOG_FILE" >> $GITHUB_OUTPUT + + - name: Check header is present in changelog file + run: | + FILE="${{ steps.file-presence.outputs.FILE }}" + VERSION="$(php build/dump-version-info.php base_version)" + if ! grep -Fqx "# $VERSION" "${{ github.workspace }}/$FILE"; then + echo "::error::Header for $VERSION not found in $FILE" + exit 1 + fi + + - name: Check version is valid for the selected channel + run: | + CHANNEL="$(php build/dump-version-info.php channel)" + if [ "$(php build/dump-version-info.php suffix_valid)" != "true" ]; then + echo "::error::Version $(php build/dump-version-info.php base_version) is not allowed on the $CHANNEL channel" + exit 1 + fi diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index c47a4399b..0a07a738b 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -1,19 +1,29 @@ name: Draft release on: - push: - tags: "*" + workflow_call: + outputs: + draft-url: + description: 'The URL of the draft release' + value: ${{ jobs.draft.outputs.draft-url }} + version: + description: 'PocketMine-MP version' + value: ${{ jobs.draft.outputs.version }} jobs: draft: name: Create GitHub draft release - if: "startsWith(github.event.head_commit.message, 'Release ')" + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: php-version: [8.2] + outputs: + draft-url: ${{ steps.create-draft.outputs.html_url }} + version: ${{ steps.get-pm-version.outputs.PM_VERSION }} + steps: - uses: actions/checkout@v4 with: @@ -53,12 +63,11 @@ jobs: - name: Get PocketMine-MP release version id: get-pm-version run: | - echo PM_VERSION=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BASE_VERSION;') >> $GITHUB_OUTPUT - echo MCPE_VERSION=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\network\mcpe\protocol\ProtocolInfo::MINECRAFT_VERSION_NETWORK;') >> $GITHUB_OUTPUT - echo PM_VERSION_SHORT=$(php -r 'require "vendor/autoload.php"; $v = explode(".", \pocketmine\VersionInfo::BASE_VERSION); array_pop($v); echo implode(".", $v);') >> $GITHUB_OUTPUT - echo PM_VERSION_MD=$(php -r 'require "vendor/autoload.php"; echo str_replace(".", "", \pocketmine\VersionInfo::BASE_VERSION);') >> $GITHUB_OUTPUT - echo CHANGELOG_SUFFIX=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BUILD_CHANNEL === "stable" ? "" : "-" . \pocketmine\VersionInfo::BUILD_CHANNEL;') >> $GITHUB_OUTPUT - echo PRERELEASE=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BUILD_CHANNEL === "stable" ? "false" : "true";') >> $GITHUB_OUTPUT + echo PM_VERSION=$(php build/dump-version-info.php base_version) >> $GITHUB_OUTPUT + echo MCPE_VERSION=$(php build/dump-version-info.php mcpe_version) >> $GITHUB_OUTPUT + echo CHANGELOG_FILE_NAME=$(php build/dump-version-info.php changelog_file_name) >> $GITHUB_OUTPUT + echo CHANGELOG_MD_HEADER=$(php build/dump-version-info.php changelog_md_header) >> $GITHUB_OUTPUT + echo PRERELEASE=$(php build/dump-version-info.php prerelease) >> $GITHUB_OUTPUT - name: Generate PHP binary download URL id: php-binary-url @@ -91,6 +100,7 @@ jobs: - name: Create draft release uses: ncipollo/release-action@v1.14.0 + id: create-draft with: artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json,${{ github.workspace }}/core-permissions.rst commit: ${{ github.sha }} @@ -99,9 +109,10 @@ jobs: name: PocketMine-MP ${{ steps.get-pm-version.outputs.PM_VERSION }} tag: ${{ steps.get-pm-version.outputs.PM_VERSION }} token: ${{ secrets.GITHUB_TOKEN }} + skipIfReleaseExists: true #for release PRs, tags will be created on release publish and trigger the tag release workflow - don't create a second draft body: | **For Minecraft: Bedrock Edition ${{ steps.get-pm-version.outputs.MCPE_VERSION }}** - Please see the [changelogs](${{ github.server_url }}/${{ github.repository }}/blob/${{ steps.get-pm-version.outputs.PM_VERSION }}/changelogs/${{ steps.get-pm-version.outputs.PM_VERSION_SHORT }}${{ steps.get-pm-version.outputs.CHANGELOG_SUFFIX }}.md#${{ steps.get-pm-version.outputs.PM_VERSION_MD }}) for details. + Please see the [changelogs](${{ github.server_url }}/${{ github.repository }}/blob/${{ steps.get-pm-version.outputs.PM_VERSION }}/changelogs/${{ steps.get-pm-version.outputs.CHANGELOG_FILE_NAME }}#${{ steps.get-pm-version.outputs.CHANGELOG_MD_HEADER }}) for details. :information_source: Download the recommended PHP binary [here](${{ steps.php-binary-url.outputs.PHP_BINARY_URL }}). diff --git a/build/dump-version-info.php b/build/dump-version-info.php new file mode 100644 index 000000000..8898d7cab --- /dev/null +++ b/build/dump-version-info.php @@ -0,0 +1,86 @@ + $options + */ +$options = [ + "base_version" => VersionInfo::BASE_VERSION, + "mcpe_version" => ProtocolInfo::MINECRAFT_VERSION_NETWORK, + "is_dev" => VersionInfo::IS_DEVELOPMENT_BUILD, + "changelog_file_name" => function() : string{ + $version = VersionInfo::VERSION(); + $result = $version->getMajor() . "." . $version->getMinor(); + $suffix = $version->getSuffix(); + if($suffix !== ""){ + if(preg_match('/^([A-Za-z]+)(\d+)$/', $suffix, $matches) !== 1){ + fwrite(STDERR, "error: invalid current version suffix \"$suffix\"; aborting" . PHP_EOL); + exit(1); + } + $baseSuffix = $matches[1]; + $result .= "-" . strtolower($baseSuffix); + } + return $result . ".md"; + }, + "changelog_md_header" => fn() : string => str_replace(".", "", VersionInfo::BASE_VERSION), + "prerelease" => fn() : bool => VersionInfo::VERSION()->getSuffix() !== "", + "channel" => VersionInfo::BUILD_CHANNEL, + "suffix_valid" => function() : bool{ + //TODO: maybe this should be put into its own script? + $suffix = VersionInfo::VERSION()->getSuffix(); + if(VersionInfo::BUILD_CHANNEL === "stable"){ + //stable builds may not have suffixes + return $suffix === ""; + } + if(VersionInfo::BUILD_CHANNEL === "alpha" || VersionInfo::BUILD_CHANNEL === "beta"){ + $upperChannel = strtoupper(VersionInfo::BUILD_CHANNEL); + $upperSuffix = strtoupper($suffix); + return str_starts_with($upperSuffix, $upperChannel) && is_numeric(substr($upperSuffix, strlen($upperChannel))); + } + return true; + } +]; +if(count($argv) !== 2 || !isset($options[$argv[1]])){ + fwrite(STDERR, "Please provide an option (one of: " . implode(", ", array_keys($options)) . PHP_EOL); + exit(1); +} + +$result = $options[$argv[1]]; +if($result instanceof Closure){ + $result = $result(); +} +if(is_bool($result)){ + echo $result ? "true" : "false"; +}else{ + echo $result; +} diff --git a/build/make-release.php b/build/make-release.php index 7a570eb35..741f9d787 100644 --- a/build/make-release.php +++ b/build/make-release.php @@ -86,7 +86,8 @@ function systemWrapper(string $command, string $errorMessage) : void{ function main() : void{ $filteredOpts = []; - foreach(Utils::stringifyKeys(getopt("", ["current:", "next:", "channel:", "help"])) as $optName => $optValue){ + $postCommitOnly = false; + foreach(Utils::stringifyKeys(getopt("", ["current:", "next:", "channel:", "help", "post"])) as $optName => $optValue){ if($optName === "help"){ fwrite(STDOUT, "Options:\n"); @@ -96,6 +97,10 @@ function main() : void{ } exit(0); } + if($optName === "post"){ + $postCommitOnly = true; + continue; + } if(!is_string($optValue)){ fwrite(STDERR, "--$optName expects exactly 1 value\n"); exit(1); @@ -141,20 +146,25 @@ function main() : void{ $channel ??= "stable"; } - echo "About to tag version $currentVer. Next version will be $nextVer.\n"; - echo "$currentVer will be published on release channel \"$channel\".\n"; - echo "please add appropriate notes to the changelog and press enter..."; - fgets(STDIN); - systemWrapper('git add "' . dirname(__DIR__) . '/changelogs"', "failed to stage changelog changes"); - system('git diff --cached --quiet "' . dirname(__DIR__) . '/changelogs"', $result); - if($result === 0){ - echo "error: no changelog changes detected; aborting\n"; - exit(1); - } $versionInfoPath = dirname(__DIR__) . '/src/VersionInfo.php'; - replaceVersion($versionInfoPath, $currentVer->getBaseVersion(), false, $channel); - systemWrapper('git commit -m "Release ' . $currentVer->getBaseVersion() . '" --include "' . $versionInfoPath . '"', "failed to create release commit"); - systemWrapper('git tag ' . $currentVer->getBaseVersion(), "failed to create release tag"); + + if($postCommitOnly){ + echo "Skipping release commit & tag. Bumping to next version $nextVer directly.\n"; + }else{ + echo "About to tag version $currentVer. Next version will be $nextVer.\n"; + echo "$currentVer will be published on release channel \"$channel\".\n"; + echo "please add appropriate notes to the changelog and press enter..."; + fgets(STDIN); + systemWrapper('git add "' . dirname(__DIR__) . '/changelogs"', "failed to stage changelog changes"); + system('git diff --cached --quiet "' . dirname(__DIR__) . '/changelogs"', $result); + if($result === 0){ + echo "error: no changelog changes detected; aborting\n"; + exit(1); + } + replaceVersion($versionInfoPath, $currentVer->getBaseVersion(), false, $channel); + systemWrapper('git commit -m "Release ' . $currentVer->getBaseVersion() . '" --include "' . $versionInfoPath . '"', "failed to create release commit"); + systemWrapper('git tag ' . $currentVer->getBaseVersion(), "failed to create release tag"); + } replaceVersion($versionInfoPath, $nextVer->getBaseVersion(), true, $channel); systemWrapper('git add "' . $versionInfoPath . '"', "failed to stage changes for post-release commit"); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2fde67d6c..b96e4348d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -40,6 +40,7 @@ parameters: - build/php dynamicConstantNames: - pocketmine\VersionInfo::IS_DEVELOPMENT_BUILD + - pocketmine\VersionInfo::BUILD_CHANNEL - pocketmine\DEBUG - pocketmine\IS_DEVELOPMENT_BUILD stubFiles: