Skip to content

Commit

Permalink
Use GitHub App & REST PUT API for updating version.sbt
Browse files Browse the repository at this point in the history
This changes the way we authenticate and make the 1 or 2 updates to `version.sbt`
required for a release.

Before:

* Authenticate as: default `github-actions` bot
* version.sbt update method: Cherry-pick the commits created by sbt-release, then push
  them to GitHub using `git push`, with the default `github-actions` bot using its
  credentials to make the push

After:

* Authenticate as: `gu-scala-library-release` GitHub App - https://github.com/apps/gu-scala-library-release
* version.sbt update method: GitHub REST API for Repository Contents (PUT /repos/{owner}/{repo}/contents/{path})
  https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#create-or-update-file-contents
  Now we're not really specifying the whole commit, just the content change to one file.

This has a few different benefits:

* Addresses the need to coexist with our branch-protection rulesets, because GitHub Apps
  can be exempted from rules, as discussed in issue #5
* Produces `Verified` commits - the commits show up as `Verified` in the GitHub UI, and have a
  `gpgsig` header entry that is signed by GitHub itself, essentially GitHub attesting that
  the author of the commit authenticated with GitHub to perform the file update.
  https://git-scm.com/docs/signature-format#_commit_signatures
  https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#signature-verification-for-bots
  https://blog.gitbutler.com/signing-commits-in-git-explained/#github-verification

The commits now appear to be attributed to `gu-scala-library-release`, rather than, eg,
'@rtyley using gha-scala-library-release-workflow' - it's a bit of shame that the person
triggering the release is no longer so clearly visible, but it's probably less confusing.
To compensate for that, the commit message itself has been updated to specifically state
the responsible user.
  • Loading branch information
rtyley committed Feb 18, 2024
1 parent 26b2cde commit b5df698
Showing 1 changed file with 58 additions and 24 deletions.
82 changes: 58 additions & 24 deletions .github/workflows/reusable-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ 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: false # ...but if you're not the Guardian, you'll want to set this explicitly
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!
Expand All @@ -28,6 +35,11 @@ on:
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'"
Expand Down Expand Up @@ -143,9 +155,16 @@ jobs:
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
Expand All @@ -161,47 +180,54 @@ jobs:
env:
KEY_FINGERPRINT: ${{ needs.init.outputs.key_fingerprint }}
KEY_EMAIL: ${{ needs.init.outputs.key_email }}
GH_TOKEN: ${{ steps.generate-github-app-token.outputs.token }}
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
VERSION_FILE_PATH="version.sbt" # TODO, work it out from diff?
VERSION_FILE_INITIAL_SHA=$( git rev-parse $GITHUB_REF:$VERSION_FILE_PATH )
VERSION_FILE_RELEASE_SHA=$( git rev-parse $RELEASE_TAG:$VERSION_FILE_PATH )
VERSION_FILE_RELEASE_CONTENT=$( git cat-file blob $RELEASE_TAG:$VERSION_FILE_PATH | base64 )
VERSION_FILE_POST_RELEASE_CONTENT=$( git cat-file blob unsigned/$GITHUB_REF_NAME:$VERSION_FILE_PATH | base64 )
echo "VERSION_FILE_PATH=$VERSION_FILE_PATH"
echo "VERSION_FILE_INITIAL_SHA=$VERSION_FILE_INITIAL_SHA"
echo "VERSION_FILE_RELEASE_SHA=$VERSION_FILE_RELEASE_SHA"
echo "VERSION_FILE_RELEASE_CONTENT=$VERSION_FILE_RELEASE_CONTENT"
echo "VERSION_FILE_POST_RELEASE_CONTENT=$VERSION_FILE_POST_RELEASE_CONTENT"
branch_for_pushing="preliminary-${{ github.run_id }}"
release_commit_id=$(gh api --method PUT /repos/:owner/:repo/contents/$VERSION_FILE_PATH \
--field branch="$branch_for_pushing" \
--field message="Put da stuff" \
--field sha="$VERSION_FILE_INITIAL_SHA" \
--field content="$VERSION_FILE_RELEASE_CONTENT" --jq '.commit.sha')
release_commit_id=$(git rev-parse HEAD^)
if [ "${{ needs.init.outputs.release_type }}" == "FULL_MAIN_BRANCH" ]
then
echo "Full Main-Branch release, making 2nd update (new snapshot version) on the default branch"
gh api --method PUT /repos/:owner/:repo/contents/$VERSION_FILE_PATH \
--field branch="$branch_for_pushing" \
--field message="Put da post release stuff" \
--field sha="$VERSION_FILE_RELEASE_SHA" \
--field content="$VERSION_FILE_POST_RELEASE_CONTENT"
fi
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
Expand Down Expand Up @@ -350,10 +376,14 @@ jobs:
env:
RELEASE_TAG: ${{ needs.push-release-commit.outputs.release_tag }}
RELEASE_VERSION: ${{ needs.push-release-commit.outputs.release_version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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"
Expand All @@ -367,11 +397,15 @@ jobs:
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 }}:
Expand Down

0 comments on commit b5df698

Please sign in to comment.