SFTI Release #3
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: Release Pipeline | |
on: | |
workflow_dispatch: | |
inputs: | |
version: | |
description: "Release version tag (required format: v[mayor].[minor].[bugfix], e.g. v1.3.0)" | |
type: string | |
required: true | |
draft: | |
description: Should a release draft be created? (Otherwise the release will be published immediately) | |
type: boolean | |
default: true | |
required: true | |
artifact: | |
description: Add artifacts to the release. All files from /docs are added to the release. | |
type: boolean | |
default: true | |
required: false | |
force: | |
description: "If force is true, already published releases can be overwritten. Caution: This action deletes already published releases and can **not** be undone!" | |
type: boolean | |
required: false | |
jobs: | |
release: | |
runs-on: ubuntu-latest | |
permissions: | |
contents: write | |
pull-requests: write | |
steps: | |
- name: Check user permission | |
id: permission_check | |
run: | | |
if [[ "${{ github.actor }}" != "msacrea" && "${{ github.actor }}" != "micmuell" && "${{ github.actor }}" != "dkoeni" && "${{ github.actor }}" != "juergen-petry" ]]; then | |
echo "You have no permissons to start the release action." | |
exit 1 | |
fi | |
- name: Validate input parameters | |
id: param_check | |
run: | | |
VERSION_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+$" | |
version=${{ github.event.inputs.version }} | |
if [[ ! "$version" =~ $VERSION_REGEX ]]; then | |
echo "Invalid version format: $version . Please provide a version matching the pattern 'v[number].[number].[number]'." | |
exit 1 | |
fi | |
- name: Checkout repository | |
uses: actions/checkout@v4 | |
- name: Checkout Wiki | |
uses: actions/checkout@v4 | |
with: | |
repository: ${{github.repository}}.wiki | |
path: wiki | |
- name: Extract variables for Release # Adjust names only here | |
id: var | |
run: | | |
version=$(echo ${{ github.event.inputs.version }} | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') # alternative: '[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]' | |
repo_name=$( echo ${{ github.repository }} | sed -E "s/^.*\///" ) | |
echo "VERSION"=$version >> $GITHUB_OUTPUT | |
echo "RELEASE_NAME=$repo_name Release $version" >> $GITHUB_OUTPUT | |
echo "RELEASE_TAG=v$version" >> $GITHUB_OUTPUT | |
echo "RELEASE_BRANCH=main" >> $GITHUB_OUTPUT | |
echo "RELEASE_NOTES=RELEASE.md" >> $GITHUB_OUTPUT | |
echo "RELEASE_ASSETS_FOLDER=docs/" >> $GITHUB_OUTPUT | |
echo "RELEASE_ASSETS_NAME=artifact" >> $GITHUB_OUTPUT | |
- name: Get release note content from wiki | |
id: release_note_body | |
run: | | |
regex="### Release ${{ steps.var.outputs.RELEASE_TAG }}[[:space:]]*[[:print:]]*[[:cntrl:]]{2}([[:print:]]+[[:space:]])*" | |
content=$(grep -ozE "$regex" wiki/Roadmap.md | tr "\0" "\n" | tail -n +3) | |
# abort if content is empty --> wiki page must be present before release | |
if [ -z "$content" ]; then | |
echo "Found no data for ${{ steps.var.outputs.RELEASE_NAME }} at wiki/Roadmap (${{ github.repository }}). Please create a section <<Release ${{ steps.var.outputs.RELEASE_TAG }}>> and fill in details for the release by following the instructions at the .github Wiki. A template can be found at https://github.com/swissfintechinnovations/.github/wiki/Roadmap-Example." | |
exit 1 | |
fi | |
body="$content"$'\n\n# \n\n' | |
echo "BODY=${body//$'\n'/'\n'}" >> $GITHUB_OUTPUT | |
- name: Check file version | |
id: check_version | |
run: | | |
BASE_BRANCH="${{ steps.var.outputs.RELEASE_BRANCH }}" | |
git checkout -q origin/$BASE_BRANCH | |
release_version=${{ steps.var.outputs.VERSION }} | |
versions=$(grep -Eo 'version: [0-9]+\.[0-9]+\.[0-9]+' *.yaml | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+') | |
versions_outdated='false' | |
for version in $versions; do | |
if [[ $version != $release_version ]]; then | |
versions_outdated='true' | |
break | |
fi | |
done | |
echo "VERSIONS_OUTDATED=$versions_outdated" >> $GITHUB_OUTPUT | |
- name: Switch to release branch | |
id: create_release_branch | |
if: steps.check_version.outputs.VERSIONS_OUTDATED == 'true' | |
run: | | |
BASE_BRANCH="${{ steps.var.outputs.RELEASE_BRANCH }}" | |
BRANCH_NAME="release/${{ steps.var.outputs.VERSION }}" | |
git config --global user.name "sfti bot" | |
git config --global user.email "[email protected]" | |
git checkout -q origin/$BASE_BRANCH | |
git checkout $BRANCH_NAME 2>/dev/null || git checkout -b $BRANCH_NAME # checkout branch or create new one if not exists | |
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
- name: Update version in files | |
id: update_version | |
if: steps.check_version.outputs.VERSIONS_OUTDATED == 'true' | |
run: | | |
VERSION="${{ steps.var.outputs.VERSION }}" | |
RELEASE_NAME="${{ steps.var.outputs.RELEASE_NAME }}" | |
BRANCH_NAME="${{ steps.create_release_branch.outputs.BRANCH_NAME }}" | |
echo "Prepare Release $RELEASE_NAME: update version number in yaml files" | |
sed -E -i "s/version: [0-9]+\.[0-9]+\.[0-9]+/version: $VERSION/" *.yaml | |
git add -u . | |
# commit only if there was changes in the yaml files | |
git diff --staged --quiet || git commit -m "Automated version update" | |
# git commit --quiet --allow-empty -m '[skip-workflow]' | |
git push --quiet --set-upstream origin $BRANCH_NAME | |
- name: "Rollback: Delete Branch" | |
if: failure() && steps.update_version.outcome == 'failure' | |
run: | | |
git checkout -q main | |
git branch -D ${{ steps.create_release_branch.outputs.BRANCH_NAME }} | |
git push origin --delete ${{ steps.create_release_branch.outputs.BRANCH_NAME }} | |
- name: Create and merge Pull Request | |
id: create_pr | |
if: steps.check_version.outputs.VERSIONS_OUTDATED == 'true' | |
run: | | |
BASE_BRANCH=${{ steps.var.outputs.RELEASE_BRANCH }} | |
response=$(gh pr create -B $BASE_BRANCH -H ${{ steps.create_release_branch.outputs.BRANCH_NAME }} --title 'Automated version update to version ${{ steps.var.outputs.VERSION }}' --body 'Created by Github action (release workflow)') | |
number=$(echo "$response" | grep -oE '[0-9]+$') # parse GitHub PR link to extract PR number | |
echo "PR_NUMBER=$number" >> $GITHUB_OUTPUT | |
sleep 2 | |
# gh pr review $number --approve # can not approve own PR | |
gh pr merge $number --admin --merge --delete-branch --body "Automated version update" # force merge since PR is not approved (--admin flag) | |
git checkout $BASE_BRANCH | |
env: | |
GITHUB_TOKEN: ${{ secrets.SFTI_BOT_TOKEN }} | |
- name: "Rollback: Delete PR" | |
if: failure() && steps.create_pr.outcome == 'failure' | |
run: | | |
gh pr close ${{ steps.create_pr.outputs.PR_NUMBER }} -c "Couldn't merge PR automatically" --delete-branch | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
- name: Delete release (draft) if already exists | |
id: delete_release_draft | |
run: | | |
ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }} | |
REPO_NAME=${{ github.repository }} | |
TAG=${{ steps.var.outputs.RELEASE_TAG }} | |
RELEASE_NAME="${{ steps.var.outputs.RELEASE_NAME }}" | |
# Get the release ID belonging to the release tag | |
RELEASE_ID=$(curl -sS -X GET -H "Authorization: Bearer $ACCESS_TOKEN" "https://api.github.com/repos/$REPO_NAME/releases" | jq -r ".[] | select(.name == \"$RELEASE_NAME\") | .id") | |
# RELEASE_ID var contains 0 or 1 IDs | |
if [[ ! $RELEASE_ID =~ ^[0-9]*$ ]]; then | |
echo "Found more than one release with name \"$RELEASE_ID\"." | |
exit 1 | |
fi | |
is_draft=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/$REPO_NAME/releases/$RELEASE_ID | jq '.draft') | |
if [[ "$is_draft" == 'false' ]]; then | |
echo "Release is published." | |
if [[ ${{ github.event.inputs.force }} == true ]]; then | |
commit_id=$(git rev-list -n 1 "refs/tags/$TAG") | |
echo $commit_id | |
git tag -d $TAG | |
git push origin ":refs/tags/$TAG" | |
echo "Force Flag is set! Deleted tag $TAG at commit ID $commit_id." | |
fi | |
if [[ ${{ github.event.inputs.force }} == false ]]; then | |
echo "Release $TAG already exists. Please verify that the version entered is correct. Set the force flag to overwrite the release." | |
exit 1 | |
fi | |
fi | |
if [[ -n "$RELEASE_ID" ]]; then | |
# Delete the existing release draft | |
curl -sS -X DELETE -H "Authorization: Bearer $ACCESS_TOKEN" "https://api.github.com/repos/$REPO_NAME/releases/$RELEASE_ID" | |
echo "Deleted release \"$RELEASE_ID\" with ID $RELEASE_ID" | |
fi | |
- name: Create release draft | |
id: create_release_draft | |
run: | | |
ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }} | |
REPO_NAME=${{ github.repository }} | |
RESPONSE=$(curl -sS -i -X POST \ | |
-H "Authorization: Bearer $ACCESS_TOKEN" \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
"https://api.github.com/repos/$REPO_NAME/releases" \ | |
-d '{ | |
"tag_name": "'"${{ steps.var.outputs.RELEASE_TAG }}"'", | |
"target_commitish": "'"${{ steps.var.outputs.RELEASE_BRANCH }}"'", | |
"name": "'"${{ steps.var.outputs.RELEASE_NAME }}"'", | |
"name": "'"${{ steps.var.outputs.RELEASE_NAME }}"'", | |
"body": "'"${{ steps.release_note_body.outputs.BODY }}"'", | |
"generate_release_notes": true, | |
"draft": true | |
}') | |
if [[ $(echo $RESPONSE | head -n 1 | cut -d$' ' -f2 ) -ne 201 ]]; then | |
echo "Failed to create release draft. Received response from GitHub API:" | |
echo "" | |
echo "$RESPONSE" | |
exit 1 | |
fi | |
echo "RELEASE_ID=$(echo $RESPONSE | grep -o -z '\{.*\}' | jq -r '.id')" >> $GITHUB_OUTPUT | |
- name: Upload artifacts | |
id: upload_artifact | |
# ensure assert folder exsits and not empty otherwise skip step | |
if: github.event.inputs.artifact == 'true' && (hashFiles(steps.var.outputs.RELEASE_ASSETS_FOLDER) != '') | |
run: | | |
ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }} | |
REPO_NAME=${{ github.repository }} | |
RELEASE_ID=${{ steps.create_release_draft.outputs.RELEASE_ID }} | |
RELEASE_ASSETS_FOLDER=${{ steps.var.outputs.RELEASE_ASSETS_FOLDER }} | |
RELEASE_ASSETS_NAME=${{ steps.var.outputs.RELEASE_ASSETS_NAME }} | |
RELEASE_ASSETS_ZIP=$RELEASE_ASSETS_NAME.zip | |
RELEASE_ASSETS_TARGZ=$RELEASE_ASSETS_NAME.tar.gz | |
zip -qr $RELEASE_ASSETS_ZIP $RELEASE_ASSETS_FOLDER | |
RESPONSE=$(curl -sS -i -X POST \ | |
-H "Authorization: Bearer $ACCESS_TOKEN" \ | |
-H "Content-Type: application/octet-stream" \ | |
"https://uploads.github.com/repos/$REPO_NAME/releases/$RELEASE_ID/assets?name=$RELEASE_ASSETS_ZIP" \ | |
--data-binary "@$RELEASE_ASSETS_ZIP" ) | |
if [[ $(echo $RESPONSE | head -n 1 | cut -d$' ' -f2 ) -ne 201 ]]; then | |
echo "Failed to upload release asset $RELEASE_ASSETS_ZIP. Received response from GitHub API:" | |
echo "" | |
echo "$RESPONSE" | |
exit 1 | |
fi | |
tar -czf $RELEASE_ASSETS_TARGZ $RELEASE_ASSETS_FOLDER | |
RESPONSE=$(curl -sS -i -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/octet-stream" \ | |
--data-binary "@$RELEASE_ASSETS_TARGZ" \ | |
"https://uploads.github.com/repos/$REPO_NAME/releases/$RELEASE_ID/assets?name=$RELEASE_ASSETS_TARGZ") | |
if [[ $(echo $RESPONSE | head -n 1 | cut -d$' ' -f2 ) -ne 201 ]]; then | |
echo "Failed to upload release asset $RELEASE_ASSETS_TARGZ. Received response from GitHub API:" | |
echo "" | |
echo "$RESPONSE" | |
exit 1 | |
fi | |
- name: Publish release | |
if: github.event.inputs.draft == 'false' | |
id: publish_release | |
run: | | |
ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }} | |
REPO_NAME=${{ github.repository }} | |
RELEASE_ID=${{ steps.create_release_draft.outputs.RELEASE_ID }} | |
RESPONSE=$(curl -sS -i -X PATCH \ | |
-H "Authorization: Bearer $ACCESS_TOKEN" \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
"https://api.github.com/repos/$REPO_NAME/releases/$RELEASE_ID" \ | |
-d '{ | |
"prerelease": false, | |
"draft": false, | |
"make_latest": "true" | |
}') | |
if [[ $(echo $RESPONSE | head -n 1 | cut -d$' ' -f2 ) -ne 200 ]]; then | |
echo "Failed to publish release. Received response from GitHub API:" | |
echo "" | |
echo "$RESPONSE" | |
exit 1 | |
fi |