From fac18d824a7a694fd00811f5680e3671cf0d3a22 Mon Sep 17 00:00:00 2001 From: Michal Kleiner Date: Tue, 13 Aug 2024 12:55:27 +1200 Subject: [PATCH] Add Github workflow to allow for automatic preview releases (#22367) * Add PHP method to determine next preview version number * Allow to use preview release branch in tests via an input param * Allow to fail fast in tests via an input param * Add preview release workflow * Use assertEmpty in unit tests to check if there's no preview version * Apply suggestions from code review Co-authored-by: Marc Neudert * Make tests job conditional on the prepare job outputs * Fix next preview version generation to correctly bump patch or b/rc if needed * Adjust release workflow to be able to be reused * Reuse release action to create preview releases * Tidy up for consistency * Fix name reference * Add a check whether previous non-stable version has been released * Skip release checks for -alpha as that won't ever be released * Fix output variable name * Tweak conditional logic * Fix regex by adding the missing delimiter * Use regex assert method * Fix typo in test value * Adjust release preview to require password and verify user * Set ENV variable and remove redundent check --------- Co-authored-by: Marc Neudert Co-authored-by: caddoo --- .github/workflows/matomo-tests.yml | 14 ++- .github/workflows/release-preview.yml | 169 ++++++++++++++++++++++++++ .github/workflows/release.yml | 22 +++- core/Version.php | 49 +++++++- tests/PHPUnit/Unit/VersionTest.php | 62 +++++++++- 5 files changed, 305 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/release-preview.yml diff --git a/.github/workflows/matomo-tests.yml b/.github/workflows/matomo-tests.yml index 154ce2c2440..1ff74e9486a 100644 --- a/.github/workflows/matomo-tests.yml +++ b/.github/workflows/matomo-tests.yml @@ -13,6 +13,12 @@ on: - '**.x-dev' - 'next_release' workflow_dispatch: + workflow_call: + inputs: + is_preview: + type: boolean + required: false + default: false permissions: actions: read @@ -34,7 +40,7 @@ jobs: PHP: runs-on: ubuntu-20.04 strategy: - fail-fast: false + fail-fast: ${{ inputs.is_preview == true }} matrix: type: [ 'UnitTests', 'SystemTestsPlugins', 'SystemTestsCore', 'IntegrationTestsCore', 'IntegrationTestsPlugins' ] php: [ '7.2', '8.2', '8.3' ] @@ -60,6 +66,7 @@ jobs: persist-credentials: false submodules: true path: matomo + ref: ${{ inputs.is_preview == true && '5.x-preview' || github.ref }} - name: running tests uses: matomo-org/github-action-tests@main with: @@ -81,6 +88,7 @@ jobs: persist-credentials: false submodules: true path: matomo + ref: ${{ inputs.is_preview == true && '5.x-preview' || github.ref }} - name: running tests uses: matomo-org/github-action-tests@main with: @@ -97,6 +105,7 @@ jobs: persist-credentials: false submodules: true path: matomo + ref: ${{ inputs.is_preview == true && '5.x-preview' || github.ref }} - name: running tests uses: matomo-org/github-action-tests@main with: @@ -106,7 +115,7 @@ jobs: UI: runs-on: ubuntu-20.04 strategy: - fail-fast: false + fail-fast: ${{ inputs.is_preview == true }} matrix: parts: [ 0,1,2,3 ] steps: @@ -116,6 +125,7 @@ jobs: persist-credentials: false submodules: true path: matomo + ref: ${{ inputs.is_preview == true && '5.x-preview' || github.ref }} - name: running tests uses: matomo-org/github-action-tests@main with: diff --git a/.github/workflows/release-preview.yml b/.github/workflows/release-preview.yml new file mode 100644 index 00000000000..7b6c1930856 --- /dev/null +++ b/.github/workflows/release-preview.yml @@ -0,0 +1,169 @@ +# Matomo release action for automated PREVIEW releases +# +# Required GitHub secrets: +# +# GPG_CERTIFICATE | ASCII armored or Base64 encoded GPG certificate that is used to create the signatures for the archives +# GPG_CERTIFICATE_PASS | Passphrase of the GPG key + +name: Build preview release + +permissions: + actions: read # required for the tests job + checks: none + contents: write # required to create tag and release + deployments: none + issues: read # required for the tests job + packages: none + pull-requests: read # required for the tests jobs + repository-projects: none + security-events: none + statuses: none + +on: + # TODO: remove manual dispatch after testing and enable cron + workflow_dispatch: + branches: + - 5.x-dev + inputs: + password: + description: 'Release password' + required: true + #schedule: + # - cron: '0 1 * * *' # 1am daily +env: + RELEASE_PASSWORD: ${{ secrets.RELEASE_PASSWORD }} +jobs: + prepare_preview_version: + runs-on: ubuntu-latest + outputs: + do_release: ${{ steps.changes.outputs.do_release }} + has_new_version: ${{ steps.version.outputs.has_new_version }} + steps: + - name: "Check release password" + if: ${{ github.event.inputs.password != env.RELEASE_PASSWORD }} + uses: actions/github-script@v6 + with: + script: | + core.setFailed('Release password didn\'t match.') + - name: "Check if user is allowed" + if: ${{ github.actor != 'mattab' && github.actor != 'tsteur' && github.actor != 'sgiehl' && github.actor != 'mneudert' && github.actor != 'michalkleiner' && github.actor != 'caddoo'}} + uses: actions/github-script@v6 + with: + script: | + core.setFailed('User is not allowed to release.') + - uses: actions/checkout@v4 + with: + lfs: false + fetch-tags: true + fetch-depth: 0 + + - name: Prepare git config + run: | + cat <<- EOF > $HOME/.netrc + machine github.com + login $GITHUB_ACTOR + password $GITHUB_TOKEN + machine api.github.com + login $GITHUB_ACTOR + password $GITHUB_TOKEN + EOF + chmod 600 $HOME/.netrc + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" + git config --global user.name "$GITHUB_ACTOR" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if there are any changes to create a preview release for + id: changes + run: | + LATEST_PREVIEW=$(git tag --sort=-creatordate | grep -E '\.[0-9]{14}$' | head -n 1) + + DIFF="" + if [ -n "$LATEST_PREVIEW" ]; then + # using || true to always exit either with a diff or a success exit code to not fail the whole workflow + DIFF=$(git diff $LATEST_PREVIEW..5.x-dev --unified=0 | grep -vE "^\+\+\+|---" | grep "^[+-]" | grep -v "public const VERSION = '.*';" || true) + fi + + if [ -z "$DIFF" ]; then + echo "No changes in 5.x-dev since last preview version was created." + DO_RELEASE=0 + else + DO_RELEASE=1 + fi + + echo "do_release=$DO_RELEASE" >> $GITHUB_OUTPUT + + - name: Determine new preview version number + id: version + if: steps.changes.outputs.do_release == '1' + run: | + OLD_VERSION=$(php -r "include_once 'core/Version.php'; echo \Piwik\Version::VERSION;") + NEW_VERSION=$(php -r "include_once 'core/Version.php'; \$v = new \Piwik\Version(); echo \$v->nextPreviewVersion(\Piwik\Version::VERSION);") + + if [ "$NEW_VERSION" == "" ]; then + HAS_NEW_VERSION=0 + else + HAS_NEW_VERSION=1 + fi + + echo "OLD_VERSION=$OLD_VERSION" >> $GITHUB_ENV + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + echo "has_new_version=$HAS_NEW_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Check if the previous version has been released + if: steps.changes.outputs.do_release == '1' && steps.version.outputs.has_new_version == '1' + run: | + TAG_EXISTS=$( git tag --list "$OLD_VERSION" ) + + # x.y.z-alpha would not be released, all other versions should have an existing tag (a release) + if [[ ! $OLD_VERSION =~ -alpha$ ]] && [[ -z "$TAG_EXISTS" ]]; then + echo "$OLD_VERSION (as indicated in core/Version.php) has not been released yet." + exit 1 + fi + + - name: Update 5.x-preview branch to latest 5.x-dev + if: steps.changes.outputs.do_release == '1' && steps.version.outputs.has_new_version == '1' + run: | + git checkout -B 5.x-preview + + - name: Update version file with new version + if: steps.changes.outputs.do_release == '1' && steps.version.outputs.has_new_version == '1' + run: | + sed -i "s/VERSION = '${OLD_VERSION}';/VERSION = '${NEW_VERSION}';/g" core/Version.php + + - name: Commit version file changes + if: steps.changes.outputs.do_release == '1' && steps.version.outputs.has_new_version == '1' + run: | + git add core/Version.php + git commit -m "Update version to ${NEW_VERSION}" + + - name: Push changes to 5.x-preview + if: steps.changes.outputs.do_release == '1' && steps.version.outputs.has_new_version == '1' + run: | + git push -f origin 5.x-preview + + run_matomo_tests: + needs: [prepare_preview_version] + uses: ./.github/workflows/matomo-tests.yml + if: | + always() && + needs.prepare_preview_version.result == 'success' && + needs.prepare_preview_version.outputs.do_release == '1' && + needs.prepare_preview_version.outputs.has_new_version == '1' + with: + is_preview: true + + release_preview_version: + needs: [run_matomo_tests] + uses: ./.github/workflows/release.yml + if: | + always() && + needs.prepare_preview_version.result == 'success' && + needs.run_matomo_tests.result == 'success' && + needs.prepare_preview_version.outputs.do_release == '1' && + needs.prepare_preview_version.outputs.has_new_version == '1' + with: + is_preview: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6fa380ccf2..85a1574b5e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,12 @@ on: password: description: 'Release password' required: true + workflow_call: + inputs: + is_preview: + type: boolean + required: false + default: false env: RELEASE_PASSWORD: ${{ secrets.RELEASE_PASSWORD }} @@ -39,13 +45,13 @@ jobs: runs-on: ubuntu-latest steps: - name: "Check release password" - if: ${{ github.event.inputs.password != env.RELEASE_PASSWORD }} + if: ${{ inputs.is_preview != true && github.event.inputs.password != env.RELEASE_PASSWORD }} uses: actions/github-script@v6 with: script: | core.setFailed('Release password didn\'t match.') - name: "Check if user is allowed" - if: ${{ github.actor != 'mattab' && github.actor != 'tsteur' && github.actor != 'sgiehl' && github.actor != 'mneudert' && github.actor != 'michalkleiner' && github.actor != 'caddoo'}} + if: ${{ inputs.is_preview != true && github.actor != 'mattab' && github.actor != 'tsteur' && github.actor != 'sgiehl' && github.actor != 'mneudert' && github.actor != 'michalkleiner' && github.actor != 'caddoo'}} uses: actions/github-script@v6 with: script: | @@ -97,15 +103,21 @@ jobs: exit 1 fi - if ! [[ ${GITHUB_REF#refs/heads/} =~ ^[4-9]\.x-dev$ || ${GITHUB_REF#refs/heads/} == "next_release" ]] + if ! [[ ${GITHUB_REF#refs/heads/} =~ ^[4-9]\.x-(dev|preview)$ || ${GITHUB_REF#refs/heads/} == "next_release" ]] then - echo "A tag can only be created from branches '5.x-dev' and 'next_release'. Please create the tag manually if a release needs to be built from another branch." + echo "A tag can only be created from branches '5.x-dev', '5.x-preview' or 'next_release'. Please create the tag manually if a release needs to be built from another branch." exit 1 fi if [[ ${GITHUB_REF#refs/heads/} =~ ^[4-9]\.x-dev$ && $version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]] then - echo "Only beta/preview release tags can be created from ${GITHUB_REF#refs/heads/} branch." + echo "Only beta release tags can be created from ${GITHUB_REF#refs/heads/} branch." + exit 1 + fi + + if [[ ${GITHUB_REF#refs/heads/} =~ ^[4-9]\.x-preview$ && $version =~ [0-9]{14}$ ]] + then + echo "Only preview release tags can be created from ${GITHUB_REF#refs/heads/} branch." exit 1 fi diff --git a/core/Version.php b/core/Version.php index 3641107d4f7..6b41cf16222 100644 --- a/core/Version.php +++ b/core/Version.php @@ -41,12 +41,12 @@ public function isVersionNumber($version): bool private function isNonStableVersion($version): bool { - return (bool) preg_match('/^\d+\.\d+\.\d+((-.{1,4}\d+(\.\d{14})?)|(-alpha\.\d{14}))$/i', $version); + return (bool) preg_match('/^\d+\.\d+\.\d+(-((rc|b|beta)\d+|alpha)(\.\d{14})?)$/i', $version); } public function isPreviewVersion($version): bool { - if (\preg_match('/^\d+\.\d+\.\d+((-(rc|b|beta)\d+(\.\d{14})?)|(-alpha\.\d{14}))?$/i', $version)) { + if ($this->isNonStableVersion($version)) { if (\preg_match('/\.(\d{14})$/', $version, $matches)) { $dt = DateTime::createFromFormat('YmdHis', $matches[1]); @@ -56,4 +56,49 @@ public function isPreviewVersion($version): bool return false; } + + public function nextPreviewVersion($version): string + { + if (!$this->isVersionNumber($version)) { + return ''; + } + + $dt = date('YmdHis'); + + if ($this->isPreviewVersion($version)) { + // already a preview, update dt and check it's newer + $newVersion = substr($version, 0, -14) . $dt; + if (version_compare($version, $newVersion, '<')) { + return $newVersion; + } + return ''; + } elseif ($this->isStableVersion($version)) { + // no suffix yet, we need to bump the patch first + $newVersion = preg_replace_callback( + '/^(\d+\.\d+\.)(\d+)$/', + function ($matches) { + $matches[2] = $matches[2] + 1; + return $matches[1] . $matches[2]; + }, + $version + ); + + return sprintf('%s-alpha.%s', $newVersion, $dt); + } elseif ('alpha' === substr($version, -5)) { + // -alpha + return $version . '.' . $dt; + } else { + // -b1, -rc1 + $newVersion = preg_replace_callback( + '/^(\d+\.\d+\.\d+-(?:rc|b|beta))(\d+)$/i', + function ($matches) { + $matches[2] = $matches[2] + 1; + return $matches[1] . $matches[2]; + }, + $version + ); + + return $newVersion . '.' . $dt; + } + } } diff --git a/tests/PHPUnit/Unit/VersionTest.php b/tests/PHPUnit/Unit/VersionTest.php index 1370693a1f7..66d6369b526 100644 --- a/tests/PHPUnit/Unit/VersionTest.php +++ b/tests/PHPUnit/Unit/VersionTest.php @@ -35,14 +35,18 @@ public function testIsStableVersion() $this->assertNotStableVersion('3-3-3'); $this->assertNotStableVersion('a3.3.3'); $this->assertNotStableVersion('3.0.0b'); + $this->assertNotStableVersion('3.3.3-alpha'); $this->assertNotStableVersion('3.3.3-b1'); $this->assertNotStableVersion('3.3.3-rc1'); + $this->assertNotStableVersion('3.3.3-rc1.20240509114000'); } public function testIsVersionNumber() { $this->assertIsVersionNumber('3.3.3'); + $this->assertIsVersionNumber('3.3.3-alpha'); $this->assertIsVersionNumber('3.3.3-b1'); + $this->assertIsVersionNumber('3.3.3-rc1.20240509114000'); $this->assertIsVersionNumber('100.999.9991-rc90'); $this->assertIsVersionNumber('100.999.9991-b90'); $this->assertIsVersionNumber('100.999.9991-beta90'); @@ -53,7 +57,8 @@ public function testIsVersionNumber() $this->assertNotVersionNumber('a3.3.3'); $this->assertNotVersionNumber('3.0.0b'); $this->assertNotVersionNumber('3.0.0beta1'); // missing dash - $this->assertNotVersionNumber('3.3.3-bbeta1'); // max 4 allowed but bbeta is 5 + $this->assertNotVersionNumber('3.3.3-bbeta1'); // unknown stability + $this->assertNotVersionNumber('3.3.3-rc1.2024'); // short preview } public function testIsPreviewVersion() @@ -68,6 +73,7 @@ public function testIsPreviewVersion() $this->assertNotPreviewVersion('3-3-3'); $this->assertNotPreviewVersion('a3.3.3'); $this->assertNotPreviewVersion('3.0.0b'); + $this->assertNotPreviewVersion('3.3.3-alpha'); $this->assertNotPreviewVersion('3.3.3-b1'); $this->assertNotPreviewVersion('3.3.3-b1.p20240509114000'); $this->assertNotPreviewVersion('3.3.3-b1.20240509114000a'); @@ -83,6 +89,38 @@ public function testIsPreviewVersion() $this->assertNotPreviewVersion('3.3.3-b1.20240509114088'); } + public function testNextPreviewVersion() + { + $this->assertNextVersionIsEmpty('3.3.3-alpha.29990101000000'); // preview is newer + $this->assertNextVersionIsEmpty('3.3.3-dev'); // unsupported stability + $this->assertNextVersionIsEmpty('p20240509114000'); + + $this->assertNextVersionExists('3.3.3'); + $this->assertNextVersionExists('3.3.3-alpha'); + $this->assertNextVersionExists('3.3.3-b1'); + $this->assertNextVersionExists('3.3.3-rc1'); + $this->assertNextVersionExists('3.3.3-alpha.20201224180000'); + $this->assertNextVersionExists('3.3.3-b1.20201224180000'); + $this->assertNextVersionExists('3.3.3-rc1.20201224180000'); + } + + public function testNextPreviewCorrectlyBumpsVersionIfNeeded() + { + // stable bumps patch and adds alpha + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3', '3.3.4-alpha'); + + // non-stable bumps b1, rc1 + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-b1', '3.3.3-b2'); + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-rc1', '3.3.3-rc2'); + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-b1.20201224180000', '3.3.3-b1'); + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-rc1.20201224180000', '3.3.3-rc1'); + + // preview does not bump x.y.z, only dt suffix + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-alpha.20201224180000', '3.3.3-alpha'); + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-b1.20201224180000', '3.3.3-b1'); + $this->assertCorrectPreviewVersionWithoutSuffix('3.3.3-rc1.20201224180000', '3.3.3-rc1'); + } + private function assertIsStableVersion($versionNumber) { $isStable = $this->version->isStableVersion($versionNumber); @@ -119,10 +157,30 @@ private function assertNotPreviewVersion($versionNumber) $this->assertFalse($isPreviewVersion); } + private function assertNextVersionIsEmpty($versionNumber) + { + $nextVersionNumber = $this->version->nextPreviewVersion($versionNumber); + $this->assertEmpty($nextVersionNumber); + } + + private function assertNextVersionExists($versionNumber) + { + $nextVersionNumber = $this->version->nextPreviewVersion($versionNumber); + $this->assertTrue($this->version->isPreviewVersion($nextVersionNumber)); + } + + private function assertCorrectPreviewVersionWithoutSuffix($versionNumber, $newVersionNumber) + { + $this->assertRegExp( + "/^$newVersionNumber.\d{14}$/", + $this->version->nextPreviewVersion($versionNumber) + ); + } + /** * @dataProvider getLowerVersionCompares */ - public function testVersionContraints($v1, $v2) + public function testVersionConstraints($v1, $v2) { $v = new VersionParser(); $v1p = $v->parseConstraints($v1);