Release #7
This file contains hidden or 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 | |
on: | |
workflow_dispatch: | |
inputs: | |
bump_level: | |
description: "Version increment level" | |
type: choice | |
options: | |
- patch | |
- minor | |
- major | |
default: minor | |
workflow_run: | |
workflows: ["Test"] | |
types: [completed] | |
permissions: | |
contents: write | |
concurrency: | |
group: ${{ github.workflow }} | |
cancel-in-progress: true | |
jobs: | |
release: | |
if: > | |
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || | |
github.event_name == 'workflow_dispatch' | |
runs-on: ubuntu-latest | |
env: | |
PACKAGE_JSON: package.json | |
steps: | |
- name: Resolve target | |
id: target | |
shell: bash | |
run: | | |
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then | |
echo "ref=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT" | |
echo "sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" | |
else | |
echo "ref=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" | |
echo "sha=" >> "$GITHUB_OUTPUT" | |
fi | |
- uses: actions/checkout@v4 | |
with: | |
ref: ${{ steps.target.outputs.sha || steps.target.outputs.ref }} | |
fetch-depth: 0 | |
- name: Establish baseline | |
id: base | |
shell: bash | |
run: | | |
if [[ -n "${{ steps.target.outputs.sha }}" ]]; then | |
echo "sha=${{ steps.target.outputs.sha }}" >> "$GITHUB_OUTPUT" | |
else | |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
fi | |
echo "ref=${{ steps.target.outputs.ref }}" >> "$GITHUB_OUTPUT" | |
- name: Cancel if branch advanced | |
id: guard | |
shell: bash | |
run: | | |
ref="${{ steps.base.outputs.ref }}" | |
git fetch origin "$ref" --quiet | |
remote_sha="$(git rev-parse "origin/$ref")" | |
base_sha="${{ steps.base.outputs.sha }}" | |
if [[ "$remote_sha" != "$base_sha" ]]; then | |
echo "::notice::Remote branch '$ref' advanced from $base_sha to $remote_sha; cancelling release." | |
echo "changed=true" >> "$GITHUB_OUTPUT" | |
else | |
echo "changed=false" >> "$GITHUB_OUTPUT" | |
fi | |
- name: Skipping release due to new commits | |
if: steps.guard.outputs.changed == 'true' | |
shell: bash | |
run: | | |
{ | |
echo "### Release skipped" | |
echo | |
echo "The branch has advanced after the workflow was started." | |
} >> "$GITHUB_STEP_SUMMARY" | |
- name: Get previous tag | |
if: steps.guard.outputs.changed == 'false' | |
id: prev | |
shell: bash | |
run: | | |
prev_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)" | |
echo "tag=$prev_tag" >> "$GITHUB_OUTPUT" | |
- name: Read bump level | |
if: steps.guard.outputs.changed == 'false' | |
id: bump | |
shell: bash | |
run: | | |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
echo "level=${{ inputs.bump_level }}" >> "$GITHUB_OUTPUT" | |
else | |
echo "level=patch" >> "$GITHUB_OUTPUT" | |
fi | |
echo "Selected bump level: ${{ steps.bump.outputs.level }}" | |
- name: Bump package.json | |
if: steps.guard.outputs.changed == 'false' | |
id: ver | |
shell: bash | |
run: | | |
set -euo pipefail | |
if [ ! -f "$PACKAGE_JSON" ]; then | |
echo "Package file not found at: $PACKAGE_JSON" | |
exit 1 | |
fi | |
current="$(jq -r '.version' "$PACKAGE_JSON")" | |
if [[ ! "$current" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
echo "Version '$current' is not SemVer (x.y.z)" | |
exit 1 | |
fi | |
IFS='.' read -r major minor patch <<<"$current" | |
level="${{ steps.bump.outputs.level }}" | |
case "$level" in | |
major) | |
major=$((major+1)); minor=0; patch=0 | |
;; | |
minor) | |
minor=$((minor+1)); patch=0 | |
;; | |
patch) | |
patch=$((patch+1)) | |
;; | |
*) | |
echo "Unsupported bump level: '$level' (expected: major|minor|patch)" | |
exit 1 | |
;; | |
esac | |
new="$major.$minor.$patch" | |
if [[ "$new" == "$current" ]]; then | |
echo "Version unchanged; nothing to do." | |
exit 0 | |
fi | |
tmp="$PACKAGE_JSON.tmp" | |
jq --arg v "$new" '.version=$v' "$PACKAGE_JSON" > "$tmp" | |
mv "$tmp" "$PACKAGE_JSON" | |
echo "new=$new" >> "$GITHUB_OUTPUT" | |
- name: Commit and tag | |
if: steps.guard.outputs.changed == 'false' && steps.ver.outputs.new != '' | |
id: push | |
shell: bash | |
run: | | |
set -euo pipefail | |
echo "pushed=false" >> "$GITHUB_OUTPUT" | |
git config user.name "github-actions[bot]" | |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
git add "$PACKAGE_JSON" | |
git commit -m "${{ github.repository }} ${{ steps.ver.outputs.new }} [release] [skip ci]" | |
ref="${{ steps.base.outputs.ref }}" | |
base_sha="${{ steps.base.outputs.sha }}" | |
git fetch origin "$ref" --quiet | |
remote_sha="$(git rev-parse "origin/$ref")" | |
if [[ "$remote_sha" != "$base_sha" ]]; then | |
echo "::notice::Remote branch '$ref' advanced to $remote_sha; cancelling release." | |
exit 0 | |
fi | |
if ! git push origin "HEAD:${{ steps.target.outputs.ref }}"; then | |
echo "::notice::Non-fast-forward push rejected; cancelling release." | |
exit 0 | |
fi | |
git tag "v${{ steps.ver.outputs.new }}" | |
git push origin "v${{ steps.ver.outputs.new }}" | |
echo "pushed=true" >> "$GITHUB_OUTPUT" | |
- name: Create GitHub release | |
if: steps.ver.outputs.new != '' && steps.push.outputs.pushed == 'true' | |
env: | |
GH_TOKEN: ${{ github.token }} | |
shell: bash | |
run: | | |
set -euo pipefail | |
tag="v${{ steps.ver.outputs.new }}" | |
prev_tag="${{ steps.prev.outputs.tag }}" | |
ref="${{ steps.target.outputs.ref }}" | |
upper="${{ steps.target.outputs.sha }}" | |
if [[ -z "$upper" ]]; then | |
upper="$(git rev-parse HEAD)" | |
fi | |
declare -A since=() | |
if [[ -n "${prev_tag:-}" && "$prev_tag" != "$tag" ]]; then | |
while read -r s; do | |
[[ -n "$s" ]] && since["$s"]=1 | |
done < <(git rev-list "${prev_tag}..$upper") | |
fi | |
notes="release-notes.md" | |
{ | |
echo "#### The latest changes that went into this release:" | |
echo | |
echo '| Author | Release | Commit | SHA |' | |
echo '|---|---|---|---|' | |
} > "$notes" | |
limit=10 | |
count=0 | |
mapfile -t rel_tags < <(git for-each-ref --sort=creatordate --format='%(refname:short)' refs/tags | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true) | |
git log -n 20 "$upper" --pretty=format:%H%x09%s%x09%an%x09%ae | \ | |
while IFS=$'\t' read -r sha msg author email; do | |
lc_author="$(printf '%s' "$author" | tr '[:upper:]' '[:lower:]')" | |
lc_email="$(printf '%s' "$email" | tr '[:upper:]' '[:lower:]')" | |
if [[ "$lc_author" == "github-actions[bot]" || "$lc_email" == "41898282+github-actions[bot]@users.noreply.github.com" ]]; then | |
continue | |
fi | |
short="${sha:0:7}" | |
msg="${msg//|/\\|}" | |
new_prefix="" | |
if [[ -n "${since[$sha]:-}" ]]; then | |
new_prefix="✨ " | |
fi | |
info="$(gh api repos/$GITHUB_REPOSITORY/commits/$sha --jq '{email: .commit.author.email, login: (.author.login // .committer.login)}')" | |
login="$(jq -r '.login // empty' <<<"$info")" | |
if [[ -n "$login" && "$login" != "null" ]]; then | |
avatar="https://wsrv.nl/?url=github.com/${login}.png&w=48&h=48&mask=circle&fit=cover&maxage=1w" | |
else | |
email_lookup="$(jq -r '.email // empty' <<<"$info" | tr '[:upper:]' '[:lower:]')" | |
if [[ -n "$email_lookup" ]]; then | |
hash="$(printf '%s' "$email_lookup" | md5sum | awk '{print $1}')" | |
base="www.gravatar.com/avatar/$hash?d=identicon" | |
else | |
base="www.gravatar.com/avatar/?d=mp" | |
fi | |
avatar="https://wsrv.nl/?url=${base}&w=48&h=48&mask=circle&fit=cover&maxage=1w" | |
fi | |
author_text="$author" | |
author_text="${author_text//|/\\|}" | |
if [[ -n "$login" && "$login" != "null" ]]; then | |
author_cell="<a href=\"https://github.com/$login\"><img src=\"$avatar\" width=\"24\" height=\"24\" /></a> @$login" | |
else | |
author_cell="<img src=\"$avatar\" width=\"24\" height=\"24\" /> $author_text" | |
fi | |
version="-" | |
for t in "${rel_tags[@]}"; do | |
tcommit="$(git rev-list -n 1 "$t")" | |
if git merge-base --is-ancestor "$sha" "$tcommit"; then | |
version="$t" | |
break | |
fi | |
done | |
version="${new_prefix}${version}" | |
printf '| %s | %s | %s | `%s` |\n' "$author_cell" "$version" "$msg" "$short" >> "$notes" | |
count=$((count+1)) | |
if [[ $count -ge $limit ]]; then | |
break | |
fi | |
done | |
if gh release view "$tag" >/dev/null 2>&1; then | |
echo "Release $tag already exists; skipping." | |
else | |
gh release create "$tag" --title "$tag" --notes-file "$notes" --target "$ref" | |
fi | |
{ | |
echo "## $tag" | |
echo | |
cat "$notes" | |
} >> "$GITHUB_STEP_SUMMARY" | |
- name: Update latest branch | |
if: steps.ver.outputs.new != '' && steps.push.outputs.pushed == 'true' | |
shell: bash | |
run: | | |
set -euo pipefail | |
git push -f origin HEAD:refs/heads/release |