Skip to content

Commit

Permalink
NEW Add gauge release and dispatching gha-auto-tag. (#25)
Browse files Browse the repository at this point in the history
This allows for a dispatchable patch tagging workflow, which means we
can remove the need for `contents:write` in the main ci.yml.
GuySartorelli authored Jul 31, 2024
1 parent ef07d87 commit db0929f
Showing 2 changed files with 317 additions and 27 deletions.
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -17,17 +17,43 @@ jobs:
- name: Create tag and release
uses: silverstripe/gha-tag-release@v1
with:
skip_gauge_release: true
tag: 1.2.3
release: true
release: false
```
### Inputs
#### tag (required)
The tag to create e.g. 1.2.3
### Latest local sha
Required if not skipping gauge release.
The result of `$(git rev-parse HEAD)`.

`latest_local_sha: f22dbc6ec6118096c8ccccee1ca0074bfb2f2291`

### Skip gauge release
Whether to skip gauging the release. Gauging the release will only allow tagging if all of the following are true:

- The branch this action is run against is a patch branch (e.g. `1.2`)
- The `latest_local_sha` input matches the `github.sha` GitHub actions variable
- The `latest_local_sha` input matches the current latest commit sha on this branch
- There's an existing stable semver patch tag for this branch already (i.e. for branch `1.2` there must be a `1.2.<digit>` tag)
- There are commits on the branch that warrant a new patch release and weren't included in the latest patch release for this branch

Gauging the release also identifies the correct next patch tag, and uses that to tag the release. Default is false, enable with:

`skip_gauge_release: true`

#### tag
Required if skipping gauge release. Cannot be provided if _not_ skipping gauge release.

The tag to create e.g. `1.2.3`.

#### delete_existing
Cannot be provided if _not_ skipping gauge release.

Whether to delete any existing tags or releases that match tag if they exist. Default is false, enable with:

`delete_existing: true`

#### release
@@ -38,17 +64,20 @@ The description text used for the release - format with markdown

#### release_auto_notes
Whether to use the github API to auto generate the release which will be appended to `release_description`. Default is false, enable with:

`release_auto_notes: true`

## Why there is no SHA input paramater
## Why there is no SHA input parameter when not using gauge release

Creating a tag for a particular SHA, either via the GitHub API or via CLI (i.e. git tag) in an action is strangely blocked. The error is "Resource not accessible by integration" which is a permissions error.

However, tags can be created with the following methods:

- Using `${{ github.sha }}` which is the latest sha in a context instead of historic sha
- Creating a release via GitHub API, which will also create a tag. While it's tempting to just use this and then delete the release, it's seems possible that this may stop working in the future

The following methods have been attempted:

- Using third party actions to create tags
- Passing in `permissions: write-all` from the calling workflow
- Passing in a github token from the calling workflow
307 changes: 284 additions & 23 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,49 +1,282 @@
name: Tag and release
description: GitHub Action to create a tag and an optional release
description: GitHub Action to check if a patch release can be tagged, and then create a tag and an optional release

inputs:
latest_local_sha:
description: The latest local sha. Used to gauge the release
required: false
type: string
skip_gauge_release:
description: If true, skip straight to tagging the release
required: false
default: false
type: boolean
# Note: the following inputs should usually be left as default if using gauge release
# Note: there is an explicit reason why there is no sha input parameter - see the readme
tag:
description: The name of the tag. Required if skipping gauge release. Cannot use if not skipping gauge release
required: false
default: ''
type: string
required: true
delete_existing:
type: boolean
required: false
default: false
release:
type: boolean
release:
description: Whether to create a GitHub Release as well as a tag
required: false
default: false
default: true
type: boolean
release_description:
type: string
description: The description for the GitHub Release if creating one
required: false
default: ''
type: string
release_auto_notes:
description: If true, the GitHub release description is automatically generated
required: false
default: true
type: boolean
# Only set this to true in action-ci.yml
# We need this to avoid race conditions, i.e. if we dispatched this action and autotag from action-ci, autotag would likely finish first.
dispatch_gha_autotag:
description: If true, the auto-tag.yml workflow will be dispatched after tagging is successful
required: false
default: false
type: boolean

runs:
using: composite
steps:

- name: Validate inputs
shell: bash
env:
LATEST_LOCAL_SHA: ${{ inputs.latest_local_sha }}
SKIP_GAUGE_RELEASE: ${{ inputs.skip_gauge_release }}
TAG: ${{ inputs.tag }}
DELETE_EXISTING: ${{ inputs.delete_existing }}
shell: bash
run: |
git check-ref-format "tags/$TAG" > /dev/null
if [[ $? != "0" ]]; then
echo "Invalid tag"
VALID=1
# If we're not gauging release, there MUST be a tag.
if [[ $SKIP_GAUGE_RELEASE == 'true' && $TAG == '' ]]; then
echo "Must provide a tag when skip_gauge_release is true"
VALID=0
fi
# If there's a tag, it must be a valid git ref for this repo
if [[ $SKIP_GAUGE_RELEASE != 'true' && $TAG != '' ]]; then
git check-ref-format "tags/$TAG" > /dev/null
if [[ $? != "0" ]]; then
echo "Invalid tag"
VALID=0
fi
fi
# gauge release requires the latest local sha
if [[ $SKIP_GAUGE_RELEASE != 'true' && $LATEST_LOCAL_SHA == '' ]]; then
echo "Must include latest_local_sha when skip_gauge_release is false"
VALID=0
fi
# Can't delete existing tag if using gauge release
if [[ $SKIP_GAUGE_RELEASE != 'true' && $DELETE_EXISTING == 'true' ]]; then
echo "Cannot set delete_existing to true when skip_gauge_release is false"
VALID=0
fi
if [[ $VALID != 1 ]]; then
exit 1
fi
- name: Delete existing release if one exists
if: ${{ inputs.release == 'true' && inputs.delete_existing == 'true' }}
- name: Check unreleased changes
id: gauge-release
env:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF_NAME: ${{ github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
LATEST_LOCAL_SHA: ${{ inputs.latest_local_sha }}
SKIP_GAUGE_RELEASE: ${{ inputs.skip_gauge_release }}
shell: bash
run: |
DO_RELEASE=1
if [[ $SKIP_GAUGE_RELEASE == 'true' ]]; then
echo "skipping gauge release"
echo "do_release output is $DO_RELEASE"
echo "do_release=$DO_RELEASE" >> $GITHUB_OUTPUT
exit 0
fi
# Double check that LATEST_LOCAL_SHA matches GITHUB_SHA
echo "LATEST_LOCAL_SHA is $LATEST_LOCAL_SHA"
echo "GITHUB_SHA is $GITHUB_SHA"
if [[ $LATEST_LOCAL_SHA != $GITHUB_SHA ]]; then
echo "Not patch releasing because GITHUB_SHA is not equal to latest local sha"
DO_RELEASE=0
fi
# Must be on a minor branch to do a patch release
if [[ $DO_RELEASE == "1" ]]; then
if ! [[ $GITHUB_REF_NAME =~ ^[0-9]+\.[0-9]+$ ]]; then
echo "Not patch releasing because not on a minor branch"
DO_RELEASE=0
fi
fi
# Validate that this commit is that latest commit for the branch using GitHub API
# We need to check this in case re-rerunning an old job and there have been new commits since
if [[ $DO_RELEASE == "1" ]]; then
# https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28
RESP_CODE=$(curl -w %{http_code} -s -o __response.json \
-X GET "https://api.github.com/repos/${GITHUB_REPOSITORY}/commits?sha=${GITHUB_REF_NAME}&per_page=1" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
)
if [[ $RESP_CODE != "200" ]]; then
echo "Unable to read list of commits - HTTP response code was $RESP_CODE"
exit 1
fi
LATEST_REMOTE_SHA=$(jq -r '.[0].sha' __response.json)
echo "LATEST_REMOTE_SHA is $LATEST_REMOTE_SHA"
echo "LATEST_LOCAL_SHA is $LATEST_LOCAL_SHA"
if [[ $LATEST_REMOTE_SHA != $LATEST_LOCAL_SHA ]]; then
echo "Not patch releasing because latest remote sha is not equal to latest local sha"
DO_RELEASE=0
fi
# Also validate the sha matches GITHUB_SHA, which is what gha-tag-release will use
if [[ $GITHUB_SHA != $LATEST_LOCAL_SHA ]]; then
echo "Not patch releasing because GITHUB_SHA is not equal to latest local sha"
DO_RELEASE=0
fi
fi
# Check is there is an existing tag on the branch using GitHub API
# Note cannot use local `git tag` because actions/checkout by default will not checkout tags
# and you need to checkout full history in order to get them
LATEST_TAG=""
NEXT_TAG=""
if [[ $DO_RELEASE == "1" ]]; then
# https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#list-matching-references
RESP_CODE=$(curl -w %{http_code} -s -o __response.json \
-X GET https://api.github.com/repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${GITHUB_REF_NAME} \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
)
if [[ $RESP_CODE != "200" ]]; then
echo "Unable to read list of tags - HTTP response code was $RESP_CODE"
exit 1
fi
# Get the latest tag
LATEST_TAG=$(jq -r '.[].ref' __response.json | grep -Po '(?<=^refs\/tags\/)[0-9]+\.[0-9]+\.[0-9]+$' | sort -V -r | head -n 1) || true
echo "LATEST_TAG is $LATEST_TAG"
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
if ! [[ $LATEST_TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
echo "Not patch releasing because cannot find a matching semver tag on the branch"
DO_RELEASE=0
else
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
NEXT_TAG="$MAJOR.$MINOR.$((PATCH+1))"
echo "NEXT_TAG is $NEXT_TAG"
echo "next_tag=$NEXT_TAG" >> $GITHUB_OUTPUT
fi
fi
# Check if there is anything relevant commits to release using GitHub API using the tripe-dot compoare endpoint
# which will show things that are in the next-patch branch that are not in the latest tag
# Note: unlike CLI, the API endpoint results include all merged pull-requests, not commits
# Pull-requests prefixed with MNT or DOC will not be considered relevant for releasing
if [[ $DO_RELEASE == "1" ]]; then
# Check on github release notes api if there's anything worth releasing
# Compare commits between current sha with latest tag to see if there is anything worth releasing
# https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits
RESP_CODE=$(curl -w %{http_code} -s -o __response.json \
-X GET https://api.github.com/repos/$GITHUB_REPOSITORY/compare/$LATEST_TAG...$GITHUB_SHA?per_page=100 \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
)
if [[ $RESP_CODE != "200" ]]; then
echo "Unable to fetch compare two commits - HTTP response code was $RESP_CODE"
exit 1
fi
# Get commits for text parsing
jq -r '.commits[].commit.message' __response.json > __commits.json
# Parse comits one line at a time
HAS_THINGS_TO_RELEASE=0
while IFS="" read -r line || [[ -n $line ]]; do
# Remove any leading bullet points
line="${line#\* }"
line="${line#\-}"
line="${line# }"
if ! [[ "$line" =~ ^(Merge|MNT|DOC) ]] && ! [[ $line =~ ^[[:space:]]*$ ]]; then
HAS_THINGS_TO_RELEASE=1
break
fi
done < __commits.json
if [[ $HAS_THINGS_TO_RELEASE == "0" ]]; then
echo "Not patch releasing because there is nothing relevant to release"
DO_RELEASE=0
fi
fi
# Check again, this time using the double-dot syntax which will show the raw diff between the latest tag
# and the next-patch branch
# This isn't available via the github api, so screen scrape this instead. Screen scraping isn't
# great because it's brittle, however if this fails then all that happens is we tag a release that
# has no actual changes, which isn't the end of the world.
# Here we are only detecting if there are no actual changes to release, which can happen in a couple of scenarios:
# a) A change is made and tagged, and then backported to an older branch and then merged-up
# b) A change made in a previous major that we don't want to keep in current major, so
# it's reverted during the merge-up
if [[ $DO_RELEASE == "1" ]]; then
RESP_CODE=$(curl -w %{http_code} -s -o __compare.html \
-X GET https://github.com/$GITHUB_REPOSITORY/compare/$LATEST_TAG..$GITHUB_SHA
)
if [[ $RESP_CODE != "200" ]]; then
echo "Unable to fetch compare html - HTTP response code was $RESP_CODE"
exit 1
fi
PARSED=$(php -r '
$s = file_get_contents("__compare.html");
$s = strip_tags($s);
$s = str_replace("[\r\n]", " ", $s);
$s = preg_replace("# {2,}#", " ", $s);
echo $s;
')
# `|| true` needs to be suffixed otherwise an error code of 1 will be omitted when there is no grep match
IDENTICAL=$(echo $PARSED | grep "$LATEST_TAG and $GITHUB_SHA are identical") || true
if [[ $IDENTICAL != "" ]]; then
echo "Not patch releasing because there are no actual changes to release"
DO_RELEASE=0
fi
fi
echo "do_release output is $DO_RELEASE"
echo "do_release=$DO_RELEASE" >> $GITHUB_OUTPUT
- name: Get clean tag name
id: tag-name
if: ${{ steps.gauge-release.outputs.do_release == '1' }}
env:
DISCOVERED_TAG: ${{ steps.gauge-release.outputs.next_tag }}
INPUT_TAG: ${{ inputs.tag }}
shell: bash
run: |
# Provide an output with the tag to use
TAG="$DISCOVERED_TAG"
if [[ $INPUT_TAG != '' ]]; then
TAG="$INPUT_TAG"
fi
echo "tag output is $TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Delete existing release if one exists
if: ${{ steps.gauge-release.outputs.do_release == '1' && inputs.release == 'true' && inputs.delete_existing == 'true' }}
env:
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag-name.outputs.tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
shell: bash
run: |
# Get id for an existing release matching $TAG
# https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name
@@ -76,11 +309,11 @@ runs:
fi
- name: Delete existing tag if one exists
if: ${{ inputs.delete_existing == 'true' }}
shell: bash
if: ${{ steps.gauge-release.outputs.do_release == '1' && inputs.delete_existing == 'true' }}
env:
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag-name.outputs.tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
shell: bash
run: |
# Check if tag currently exists
# Note: not using https://api.github.com/repos/$GITHUB_REPOSITORY/git/refs/tags/<tag>
@@ -121,12 +354,12 @@ runs:
- name: Create tag
# Creating a release will also create a tag, so only create explicitly create tag if not creating release
if: ${{ inputs.release == 'false' }}
shell: bash
if: ${{ steps.gauge-release.outputs.do_release == '1' && inputs.release == 'false' }}
env:
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag-name.outputs.tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
shell: bash
run: |
# Create new tag via GitHub API
# https://docs.github.com/en/rest/reference/git#create-a-reference
@@ -149,13 +382,13 @@ runs:
echo "New tag $TAG created for sha ${{ github.sha }}"
- name: Create release
if: ${{ inputs.release == 'true' }}
shell: bash
if: ${{ steps.gauge-release.outputs.do_release == '1' && inputs.release == 'true' }}
env:
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag-name.outputs.tag }}
RELEASE_DESCRIPTION: ${{ inputs.release_description }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
shell: bash
run: |
# Work out if release should be marked as the latest
# Only do the for stable semver tags
@@ -218,10 +451,38 @@ runs:
fi
echo "New release $TAG created"
- name: Delete temporary files
- name: Dispatch auto tag
if: ${{ steps.gauge-release.outputs.do_release == '1' && inputs.dispatch_gha_autotag == 'true' }}
env:
GITHUB_REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.ref_name }}
shell: bash
run: |
# https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event
RESP_CODE=$(curl -w %{http_code} -s -L -o __response.json \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/$GITHUB_REPOSITORY/actions/workflows/auto-tag.yml/dispatches \
-d "{\"ref\":\"$BRANCH\"}"
)
if [[ $RESP_CODE != "204" ]]; then
echo "Failed to dispatch workflow - HTTP response code was $RESP_CODE"
cat __response.json
exit 1
fi
- name: Delete temporary files
if: always()
shell: bash
run: |
if [[ -f __response.json ]]; then
rm __response.json
fi
if [[ -f __commits.json ]]; then
rm __commits.json
fi
if [[ -f __compare.html ]]; then
rm __compare.html
fi

0 comments on commit db0929f

Please sign in to comment.