From 3e490f984f3d5f00e11a60d592835ba8b3f6a06f Mon Sep 17 00:00:00 2001 From: Roberto Tyley Date: Wed, 27 Dec 2023 17:19:29 +0000 Subject: [PATCH] Support publishing PREVIEW releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This addresses https://github.com/guardian/gha-scala-library-release-workflow/issues/10, providing support for publishing *preview* releases based off feature branches, ie PRs. The UX of doing a preview release is very similar to the existing process for doing a full release (https://github.com/guardian/gha-scala-library-release-workflow/blob/main/docs/making-a-release.md), with only these differences: * The developer needs to select the PR branch before clicking the green `Run workflow` button * The version number will be the **upcoming** version number, but with a suffix that clearly indicates this is a preview release, eg: `1.0.7-PREVIEW.feature1.2024-01-04T1230.42ed11d4`. Note that this is _not_ a `-SNAPSHOT` release, the workflow does not support `SNAPSHOT` releases. * No branches are updated by the release (ie, not the PR's feature branch, and not the default `main` branch) - the preview release commit exists as its own tagged commit, taking the latest PR commit as its parent. * GitHub release notes will not be created, but instead the PR using that branch will be updated with a comment providing details of the new release (version number, etc). Internally, these 'preview release' changes take place if a non-default branch (ie a feature branch, not `main`) is used: * The `🔒 Init` job `release_type` output is `PREVIEW_FEATURE_BRANCH` rather than `FULL_MAIN_BRANCH` * Only 1 commit is pushed by the workflow, rather than 2, and _not_ onto the branch - the single commit exists as a tagged leaf to the side of the PR branch. The 2nd commit normally needed by the full release process (incrementing the version number and adding the `-SNAPSHOT` suffix) is not needed for preview releases - there are already enough details in the `-PREVIEW` version-suffix to keep preview releases unique, even if you do many releases for 1 commit in 1 PR. * When that 1 commit is pushed, it's initially pushed with a _disposable_ Git tag - not the _release_ tag. Pushing _any_ commit requires either a branch or tag for the `git push` command to work on (you can't just push a commit id - I've tried), and there is no pre-existing suitable branch (we don't want to modify the PR feature branch) or tag (the release tag has an annotation message including the hashes of all artifact files generated by the release, and at the point when the commit is pushed, those artifacts & their hashes are not available yet), so we have to use a new, different, disposable, Git tag instead. ## Choice of version-suffix for preview releases https://semver.org/#spec-item-9 says: > 9. A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92, 1.0.0-x-y-z.--. > > 10. Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. Build metadata MUST be ignored when determining version precedence. Thus two versions that differ only in the build metadata, have the same precedence. Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85, 1.0.0+21AF26D3----117B344092BD. ### How can we prevent tooling from thinking our preview releases are stable releases? If we're not using the `-SNAPSHOT` suffix, there's a risk that tooling will assume that our preview releases are stable releases, and attempt to auto-upgrade to them. * IntelliJ automatically suggests dependency upgrades - it uses `PackageVersionNormalizer` with specific stability tokens that include 'preview' * Scala Steward raises dependency upgrade PRs - it uses `isPreRelease` which recognises `Hash` (6+ or 8 hex chars) & specific `Alpha` components that include 'preview'. See also scala-steward-org/scala-steward#1033, scala-steward-org/scala-steward#1549 etc * Scaladex uses PreRelease.scala - scalacenter/scaladex#614 Consequently, to be certain of being recognised as a pre-release, it seems wise to include these components in the version number: * 'PREVIEW' * a commit hash of at least 8 characters ## Problems with backticks Ideally we would be generating a markdown message for the PR comment with lots of backticks for styling: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks ...like this: https://github.com/rtyley/sample-project-using-gha-scala-library-release-workflow/pull/1#issuecomment-1872955682 ...but they get interpreted by BASH, and cause trouble... https://github.com/rtyley/sample-project-using-gha-scala-library-release-workflow/actions/runs/7399435634/job/20130944058 ...so for the time being this PR avoids them in the generated PR comment. --- .github/workflows/reusable-release.yml | 103 ++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index 5497b7a..d2e68de 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -41,6 +41,8 @@ jobs: outputs: key_fingerprint: ${{ steps.read-identifiers.outputs.key_fingerprint }} key_email: ${{ steps.read-identifiers.outputs.key_email }} + release_type: ${{ steps.generate-version-suffix.outputs.release_type }} + version_suffix: ${{ steps.generate-version-suffix.outputs.version_suffix }} steps: - uses: actions/setup-java@v4 with: @@ -59,6 +61,25 @@ jobs: key_fingerprint=$key_fingerprint key_email=$key_email EndOfFile + - name: Check for default branch + id: generate-version-suffix + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + default_branch=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name ${{ github.repository }}) + + if [[ "$default_branch" = $GITHUB_REF_NAME ]]; then + release_type="FULL_MAIN_BRANCH" + version_suffix="" + else + release_type="PREVIEW_FEATURE_BRANCH" + version_suffix="-PREVIEW.${GITHUB_REF_NAME//[^[:alnum:-_]]/}.$(date +%Y-%m-%dT%H%M).${GITHUB_SHA:0:8}" + fi + echo "current branch: $GITHUB_REF_NAME, release_type: $release_type, version_suffix: $version_suffix" + cat << EndOfFile >> $GITHUB_OUTPUT + release_type=$release_type + version_suffix=$version_suffix + EndOfFile generate-version-update-commits: name: 🎊 Test & Version @@ -77,7 +98,15 @@ jobs: run: | git config user.email "${{ needs.init.outputs.key_email }}" git config user.name "$COMMITTER_NAME" - sbt "release with-defaults" + + sbt_commands_file=$(mktemp) + cat << EndOfFile > $sbt_commands_file + set releaseVersion := releaseVersion.value.andThen(_ + "${{ needs.init.outputs.version_suffix }}") + release with-defaults + EndOfFile + cat $sbt_commands_file + sbt ";< $sbt_commands_file" + echo $GITHUB_WORKSPACE cd `mktemp -d` git clone --bare $GITHUB_WORKSPACE repo-with-unsigned-version-update-commits.git @@ -148,7 +177,18 @@ jobs: git log --format="%h %p %ce %s" --decorate=short -n3 git status - git push + + if [ "${{ needs.init.outputs.release_type }}" == "FULL_MAIN_BRANCH" ] + then + echo "Full Main-Branch release, pushing 2 commits to the default branch" + git push # push 2 commits (non-snapshot version, then new snapshot version) onto the default branch + else + tag_for_pushing="preliminary-${{ github.run_id }}" + echo "Preview Feature-Branch release, pushing 1 commit with the temporary tag $tag_for_pushing" + git tag -a -m "Tag created merely to allow _pushing_ the release commit, which gains the signed $RELEASE_TAG tag later on in the workflow" $tag_for_pushing $release_commit_id + git push origin $tag_for_pushing # push the single commit with a tag only + fi + create-artifacts: name: 🎊 Create artifacts @@ -240,7 +280,7 @@ jobs: echo "Message is..." cat tag-message.txt - echo "Creating tag" + echo "Creating release tag (including artifact hashes)" git tag -a -F tag-message.txt $RELEASE_TAG $RELEASE_COMMIT_ID echo "RELEASE_TAG=$RELEASE_TAG" @@ -289,20 +329,61 @@ jobs: sbt "sonatypeBundleRelease" github-release: - name: 🔒 GitHub Release - needs: [push-release-commit, sign] + name: 🔒 Update GitHub + needs: [init, push-release-commit, sign] runs-on: ubuntu-latest permissions: contents: write + pull-requests: write env: RELEASE_TAG: ${{ needs.push-release-commit.outputs.release_tag }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + GITHUB_REPO_URL: ${{ github.server_url }}/${{ github.repository }} steps: + - name: Common values + run: | + GITHUB_ACTIONS_PATH="$GITHUB_REPO_URL/actions" + GITHUB_WORKFLOW_FILE="release.yml" # Could be derived from $GITHUB_WORKFLOW_REF + GITHUB_WORKFLOW_URL="$GITHUB_ACTIONS_PATH/workflows/$GITHUB_WORKFLOW_FILE" + + cat << EndOfFile >> $GITHUB_ENV + GITHUB_WORKFLOW_FILE=$GITHUB_WORKFLOW_FILE + GITHUB_WORKFLOW_LINK=[GitHub UI]($GITHUB_WORKFLOW_URL) + GITHUB_WORKFLOW_RUN_LINK=[#${{ github.run_number }}]($GITHUB_ACTIONS_PATH/runs/${{ github.run_id }}) + EndOfFile - name: Create Github Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} + if: needs.init.outputs.release_type == 'FULL_MAIN_BRANCH' run: | - gh release create $RELEASE_TAG --verify-tag --generate-notes --notes "Release run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - name: Job summary + gh release create $RELEASE_TAG --verify-tag --generate-notes --notes "Release run: $GITHUB_WORKFLOW_RUN_LINK" + echo "GitHub Release notes: [$RELEASE_TAG]($GITHUB_REPO_URL/releases/tag/$RELEASE_TAG)" >> $GITHUB_STEP_SUMMARY + - name: Update PR + if: needs.init.outputs.release_type == 'PREVIEW_FEATURE_BRANCH' run: | - echo "GitHub Release notes: [$RELEASE_TAG](${{ github.server_url }}/${{ github.repository }}/releases/tag/$RELEASE_TAG)" >> $GITHUB_STEP_SUMMARY + cat << EndOfFile > comment_body.txt + @${{github.actor}} has published a preview version of this PR with release workflow run $GITHUB_WORKFLOW_RUN_LINK, based on commit ${{ github.sha }}: + + $RELEASE_TAG + +
+ Want to make another preview release? + + Click 'Run workflow' in the $GITHUB_WORKFLOW_LINK, specifying the $GITHUB_REF_NAME branch, or use the [GitHub CLI](https://cli.github.com/) command: + + gh workflow run $GITHUB_WORKFLOW_FILE --ref $GITHUB_REF_NAME + +
+ +
+ Want to make a full release after this PR is merged? + + Click 'Run workflow' in the $GITHUB_WORKFLOW_LINK, leaving the branch as the default, or use the [GitHub CLI](https://cli.github.com/) command: + + gh workflow run $GITHUB_WORKFLOW_FILE + +
+ EndOfFile + + cat comment_body.txt + + gh pr comment ${{ github.ref_name }} --body-file comment_body.txt >> $GITHUB_STEP_SUMMARY