Use Github App for pushing commits #4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Scala Library Release Workflow | ||
on: | ||
workflow_call: | ||
inputs: | ||
GITHUB_APP_ID: | ||
description: | ||
"App ID for a GitHub App that is allowed to push directly to the default branch. Eg, App ID on: | ||
https://github.com/organizations/guardian/settings/apps/gu-scala-library-release" | ||
default: '807361' # Only for use by the Guardian! | ||
required: true | ||
type: string | ||
SONATYPE_PROFILE_NAME: | ||
description: 'Sonatype account profile name, eg "com.gu", "org.xerial", etc (not your Sonatype username)' | ||
default: 'com.gu' # Only for use by the Guardian! | ||
required: false # Must be supplied if used by a non-Guardian project | ||
type: string | ||
SONATYPE_CREDENTIAL_HOST: | ||
description: 'The host of your SONATYPE_PROFILE_NAME, either "oss.sonatype.org" or "s01.oss.sonatype.org"' | ||
default: 'oss.sonatype.org' # The default host is going to be whatever "com.gu" is using | ||
required: false # ...but if you're not the Guardian, you'll want to set this explicitly | ||
type: string | ||
SONATYPE_USERNAME: | ||
description: 'Sonatype username' | ||
default: 'guardian.automated.maven.release' # Only for use by the Guardian! | ||
required: false # Must be supplied if used by a non-Guardian project | ||
type: string | ||
secrets: | ||
SONATYPE_PASSWORD: | ||
description: 'Password for the SONATYPE_USERNAME account - used to authenticate when uploading artifacts' | ||
required: true | ||
PGP_PRIVATE_KEY: | ||
description: | ||
"A passphrase-less PGP private key used to sign artifacts, commits, & tags. | ||
Should be in normal plaintext 'BEGIN PGP PUBLIC KEY BLOCK' (ASCII-armored) format, with no additional BASE64-encoding. | ||
The passphrase can be removed from an existing key using 'gpg --edit-key <key-id> passwd' : https://unix.stackexchange.com/a/550538/46453" | ||
required: true | ||
GITHUB_APP_PRIVATE_KEY: | ||
description: | ||
"See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps#generating-private-keys | ||
Should be in normal plaintext '-----BEGIN RSA PRIVATE KEY-----' format" | ||
required: true | ||
outputs: | ||
RELEASE_VERSION: | ||
description: "The un-prefixed version number of the release, eg '3.0.1'" | ||
value: ${{ jobs.push-release-commit.outputs.release_version }} | ||
RELEASE_TYPE: | ||
description: "Either 'FULL_MAIN_BRANCH' or 'PREVIEW_FEATURE_BRANCH' - whether this is a full release or a pre-release" | ||
value: ${{ jobs.init.outputs.release_type }} | ||
env: | ||
LOCAL_ARTIFACTS_STAGING_PATH: /tmp/artifact_staging | ||
COMMITTER_NAME: "@${{github.actor}} using gha-scala-library-release-workflow" | ||
RUN_ATTEMPT_UID: ${{ github.run_id }}-${{ github.run_attempt }} | ||
jobs: | ||
init: | ||
name: 🔒 Init | ||
runs-on: ubuntu-latest | ||
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: | ||
distribution: corretto | ||
java-version: 17 | ||
gpg-private-key: ${{ secrets.PGP_PRIVATE_KEY }} | ||
- name: Read Identifiers from Signing Key | ||
id: read-identifiers | ||
run: | | ||
key_fingerprint_and_email=$(gpg2 --list-secret-keys --list-options show-only-fpr-mbox) | ||
key_fingerprint=$(echo $key_fingerprint_and_email | awk '{print $1}') | ||
key_email=$(echo $key_fingerprint_and_email | awk '{print $2}') | ||
echo "key_fingerprint=$key_fingerprint" | ||
cat << EndOfFile >> $GITHUB_OUTPUT | ||
key_fingerprint=$key_fingerprint | ||
key_email=$key_email | ||
EndOfFile | ||
if ! [[ "$key_fingerprint" =~ ^[[:xdigit:]]{8,}$ ]]; then | ||
echo "::error title=Missing PGP key::Has PGP_PRIVATE_KEY been set correctly? https://github.com/guardian/gha-scala-library-release-workflow/blob/main/docs/credentials/supplying-credentials.md" | ||
exit 1 | ||
fi | ||
- 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 | ||
needs: init | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-java@v4 # don't 'cache: sbt', at least until https://github.com/actions/setup-java/pull/564 is merged | ||
with: | ||
distribution: corretto | ||
java-version: 17 | ||
# - name: Debug MIMA assessment | ||
# run: | | ||
# sbt "show versionPolicyFindIssues" | ||
- name: Use sbt-release to construct version.sbt updates | ||
run: | | ||
git config user.email "${{ needs.init.outputs.key_email }}" | ||
git config user.name "$COMMITTER_NAME" | ||
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 | ||
rm -Rf $GITHUB_WORKSPACE/* | ||
mv repo-with-unsigned-version-update-commits.git $GITHUB_WORKSPACE/ | ||
ls -lR $GITHUB_WORKSPACE | ||
- name: Job summary | ||
run: | | ||
echo "# Release $(git describe --tags --abbrev=0)" >> $GITHUB_STEP_SUMMARY | ||
- uses: actions/cache/save@v4 | ||
with: | ||
path: repo-with-unsigned-version-update-commits.git | ||
key: repo-with-unsigned-version-update-commits-${{ env.RUN_ATTEMPT_UID }} | ||
push-release-commit: | ||
name: 🔒 Push Release Commit | ||
needs: [generate-version-update-commits, init] | ||
permissions: | ||
contents: write | ||
runs-on: ubuntu-latest | ||
outputs: | ||
release_tag: ${{ steps.create-commit.outputs.release_tag }} | ||
release_version: ${{ steps.create-commit.outputs.release_version }} | ||
release_commit_id: ${{ steps.create-commit.outputs.release_commit_id }} | ||
steps: | ||
- id: generate-github-app-token | ||
uses: actions/create-github-app-token@v1 | ||
with: { app-id: ${{ inputs.GITHUB_APP_ID }}, private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }} } | ||
- uses: actions/checkout@v4 | ||
with: | ||
path: repo | ||
token: ${{ steps.generate-github-app-token.outputs.token }} | ||
persist-credentials: true # Allow us to push as the GitHub App, and bypass branch ruleset | ||
- uses: actions/cache/restore@v4 | ||
with: | ||
path: repo-with-unsigned-version-update-commits.git | ||
key: repo-with-unsigned-version-update-commits-${{ env.RUN_ATTEMPT_UID }} | ||
fail-on-cache-miss: true | ||
- uses: actions/setup-java@v4 | ||
with: | ||
distribution: corretto | ||
java-version: 17 | ||
gpg-private-key: ${{ secrets.PGP_PRIVATE_KEY }} | ||
- name: Create commit | ||
id: create-commit | ||
env: | ||
KEY_FINGERPRINT: ${{ needs.init.outputs.key_fingerprint }} | ||
KEY_EMAIL: ${{ needs.init.outputs.key_email }} | ||
run: | | ||
echo "GITHUB_REF_NAME=$GITHUB_REF_NAME" | ||
echo "GITHUB_REF=$GITHUB_REF" | ||
cd repo-with-unsigned-version-update-commits.git | ||
RELEASE_TAG=$(git describe --tags --abbrev=0) | ||
cd ../repo | ||
git status | ||
git config user.email "$KEY_EMAIL" | ||
git config user.name "$COMMITTER_NAME" | ||
git config commit.gpgsign true | ||
git config user.signingkey "$KEY_FINGERPRINT" | ||
git remote add unsigned ../repo-with-unsigned-version-update-commits.git | ||
git fetch unsigned | ||
git cherry-pick -S$KEY_FINGERPRINT $GITHUB_REF_NAME..unsigned/$GITHUB_REF_NAME | ||
git status | ||
release_commit_id=$(git rev-parse HEAD^) | ||
cat << EndOfFile >> $GITHUB_OUTPUT | ||
release_tag=$RELEASE_TAG | ||
release_version=${RELEASE_TAG#"v"} | ||
release_commit_id=$release_commit_id | ||
EndOfFile | ||
git log --format="%h %p %ce %s" --decorate=short -n3 | ||
git status | ||
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 release 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 only the single release version commit with a disposable tag | ||
fi | ||
create-artifacts: | ||
name: 🎊 Create artifacts | ||
needs: [init, push-release-commit] | ||
runs-on: ubuntu-latest | ||
outputs: | ||
ARTIFACT_SHA256SUMS: ${{ steps.record-hashes.outputs.ARTIFACT_SHA256SUMS }} | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
ref: ${{ needs.push-release-commit.outputs.release_commit_id }} | ||
- uses: actions/setup-java@v4 # don't 'cache: sbt', at least until https://github.com/actions/setup-java/pull/564 is resolved | ||
with: | ||
distribution: corretto | ||
java-version: 17 | ||
- name: Generate artifacts | ||
run: | | ||
cat << EndOfFile > sbt-commands.txt | ||
set every sonatypeProjectHosting := Some(xerial.sbt.Sonatype.GitHubHosting("$GITHUB_REPOSITORY_OWNER", "${GITHUB_REPOSITORY#*/}", "${{ needs.init.outputs.key_email }}")) | ||
set ThisBuild / publishTo := Some(Resolver.file("foobar", file("$LOCAL_ARTIFACTS_STAGING_PATH"))) | ||
EndOfFile | ||
cat sbt-commands.txt | ||
sbt ";< sbt-commands.txt; +publish" | ||
- name: Record SHA-256 hashes of artifacts | ||
id: record-hashes | ||
run: | | ||
sudo apt-get install hashdeep -q > /dev/null | ||
cd $LOCAL_ARTIFACTS_STAGING_PATH | ||
{ | ||
echo 'ARTIFACT_SHA256SUMS<<EOF' | ||
sha256deep -r -l . | ||
echo EOF | ||
} >> "$GITHUB_OUTPUT" | ||
- uses: actions/cache/save@v4 | ||
id: cache | ||
with: | ||
path: ${{ env.LOCAL_ARTIFACTS_STAGING_PATH }} | ||
key: unsigned-${{ env.RUN_ATTEMPT_UID }} | ||
sign: | ||
name: 🔒 Sign | ||
needs: [init, push-release-commit, create-artifacts] | ||
runs-on: ubuntu-latest | ||
env: | ||
KEY_FINGERPRINT: ${{ needs.init.outputs.key_fingerprint }} | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
path: repo | ||
ref: ${{ needs.push-release-commit.outputs.release_commit_id }} | ||
- uses: actions/cache/restore@v4 | ||
with: | ||
path: ${{ env.LOCAL_ARTIFACTS_STAGING_PATH }} | ||
key: unsigned-${{ env.RUN_ATTEMPT_UID }} | ||
fail-on-cache-miss: true | ||
- uses: actions/setup-java@v4 | ||
with: | ||
distribution: corretto | ||
java-version: 17 | ||
gpg-private-key: ${{ secrets.PGP_PRIVATE_KEY }} | ||
- name: Sign artifacts | ||
run: | | ||
echo "KEY_FINGERPRINT=$KEY_FINGERPRINT" | ||
find $LOCAL_ARTIFACTS_STAGING_PATH -type f -exec gpg -a --local-user "$KEY_FINGERPRINT" --detach-sign {} \; | ||
- name: Push signed tag | ||
env: | ||
RELEASE_TAG: ${{ needs.push-release-commit.outputs.release_tag }} | ||
RELEASE_COMMIT_ID: ${{ needs.push-release-commit.outputs.release_commit_id }} | ||
ARTIFACT_SHA256SUMS: ${{ needs.create-artifacts.outputs.ARTIFACT_SHA256SUMS }} | ||
KEY_EMAIL: ${{ needs.init.outputs.key_email }} | ||
run: | | ||
echo "RELEASE_TAG=$RELEASE_TAG" | ||
echo "RELEASE_COMMIT_ID=$RELEASE_COMMIT_ID" | ||
cd repo | ||
git config user.email "$KEY_EMAIL" | ||
git config user.name "$COMMITTER_NAME" | ||
git config tag.gpgSign true | ||
git config user.signingkey "$KEY_FINGERPRINT" | ||
cat << EndOfFile > tag-message.txt | ||
Release $RELEASE_TAG initiated by $COMMITTER_NAME | ||
$ARTIFACT_SHA256SUMS | ||
EndOfFile | ||
echo "Message is..." | ||
cat tag-message.txt | ||
echo "Creating release tag (including artifact hashes)" | ||
git tag -a -F tag-message.txt $RELEASE_TAG $RELEASE_COMMIT_ID | ||
echo "RELEASE_TAG=$RELEASE_TAG" | ||
echo "Pushing tag $RELEASE_TAG" | ||
git push origin $RELEASE_TAG | ||
- uses: actions/cache/save@v4 | ||
with: | ||
path: ${{ env.LOCAL_ARTIFACTS_STAGING_PATH }} | ||
key: signed-${{ env.RUN_ATTEMPT_UID }} | ||
sonatype-release: | ||
name: 🔒 Sonatype Release | ||
needs: sign | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/cache/restore@v4 | ||
with: | ||
path: ${{ env.LOCAL_ARTIFACTS_STAGING_PATH }} | ||
key: signed-${{ env.RUN_ATTEMPT_UID }} | ||
fail-on-cache-miss: true | ||
- name: Create tiny sbt project to perform Sonatype upload | ||
run: | | ||
cat << EndOfFile > build.sbt | ||
sonatypeBundleDirectory := new File("$LOCAL_ARTIFACTS_STAGING_PATH") | ||
sonatypeProfileName := "${{ inputs.SONATYPE_PROFILE_NAME }}" | ||
sonatypeCredentialHost := "${{ inputs.SONATYPE_CREDENTIAL_HOST }}" | ||
EndOfFile | ||
mkdir project | ||
echo 'addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21")' > project/plugins.sbt | ||
echo 'sbt.version = 1.9.8' > project/build.properties | ||
ls -lR . | ||
- uses: actions/setup-java@v4 | ||
with: | ||
distribution: corretto | ||
java-version: 17 | ||
cache: sbt # the issue described in https://github.com/actions/setup-java/pull/564 doesn't affect this step (no version.sbt) | ||
- name: Release | ||
env: | ||
SONATYPE_USERNAME: ${{ inputs.SONATYPE_USERNAME }} | ||
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} | ||
run: | | ||
sbt "sonatypeBundleRelease" | ||
github-release: | ||
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 }} | ||
RELEASE_VERSION: ${{ needs.push-release-commit.outputs.release_version }} | ||
GH_REPO: ${{ github.repository }} | ||
GITHUB_REPO_URL: ${{ github.server_url }}/${{ github.repository }} | ||
steps: | ||
- id: generate-github-app-token | ||
uses: actions/create-github-app-token@v1 | ||
with: { app-id: ${ { inputs.GITHUB_APP_ID } }, private-key: ${ { secrets.GITHUB_APP_PRIVATE_KEY } } } | ||
- 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 | ||
if: needs.init.outputs.release_type == 'FULL_MAIN_BRANCH' | ||
env: { GH_TOKEN: ${{ steps.generate-github-app-token.outputs.token }} } | ||
run: | | ||
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 with comment | ||
if: needs.init.outputs.release_type == 'PREVIEW_FEATURE_BRANCH' | ||
env: { GH_TOKEN: ${{ steps.generate-github-app-token.outputs.token }} } | ||
run: | | ||
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_VERSION | ||
<details> | ||
<summary>Want to make another preview release?</summary> | ||
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 | ||
</details> | ||
<details> | ||
<summary>Want to make a full release after this PR is merged?</summary> | ||
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 | ||
</details> | ||
EndOfFile | ||
cat comment_body.txt | ||
gh pr comment ${{ github.ref_name }} --body-file comment_body.txt >> $GITHUB_STEP_SUMMARY |