| 1 | name: Discord Release Notification |
| 2 | |
| 3 | on: |
| 4 | release: |
| 5 | types: [published] |
| 6 | |
| 7 | jobs: |
| 8 | notify: |
| 9 | name: Send Discord Webhook |
| 10 | runs-on: ubuntu-latest |
| 11 | permissions: |
| 12 | contents: read |
| 13 | |
| 14 | steps: |
| 15 | - name: Checkout |
| 16 | uses: actions/checkout@v4 |
| 17 | with: |
| 18 | fetch-depth: 0 |
| 19 | |
| 20 | - name: Build and send webhook |
| 21 | env: |
| 22 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 23 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} |
| 24 | TAG: ${{ github.event.release.tag_name }} |
| 25 | RELEASE_URL: ${{ github.event.release.html_url }} |
| 26 | IS_PRERELEASE: ${{ github.event.release.prerelease }} |
| 27 | run: | |
| 28 | if [ "$IS_PRERELEASE" = "true" ]; then |
| 29 | PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || echo "") |
| 30 | else |
| 31 | PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^$TAG$" | head -1 || echo "") |
| 32 | fi |
| 33 | |
| 34 | if [ -z "$PREV_TAG" ]; then |
| 35 | RANGE="HEAD" |
| 36 | else |
| 37 | RANGE="${PREV_TAG}..${TAG}" |
| 38 | fi |
| 39 | |
| 40 | REPO="${{ github.repository }}" |
| 41 | MAX_LINES=5 |
| 42 | |
| 43 | # Link #123 references to their GitHub issue/PR (GitHub redirects |
| 44 | # /issues/N to /pull/N automatically when N is a PR). |
| 45 | linkify() { |
| 46 | sed -E "s~(^|[^A-Za-z0-9_])#([0-9]+)~\1[#\2](https://github.com/${REPO}/issues/\2)~g" |
| 47 | } |
| 48 | |
| 49 | # Keep only the first MAX_LINES entries of a category, appending "…" when trimmed. |
| 50 | truncate_lines() { |
| 51 | local input="$1" total |
| 52 | [ -z "$input" ] && return |
| 53 | total=$(printf '%s\n' "$input" | wc -l) |
| 54 | if [ "$total" -gt "$MAX_LINES" ]; then |
| 55 | printf '%s\n' "$input" | head -n "$MAX_LINES" |
| 56 | echo "…" |
| 57 | else |
| 58 | printf '%s\n' "$input" |
| 59 | fi |
| 60 | } |
| 61 | |
| 62 | FEATURES=$(git log $RANGE --pretty=format:"- %s (%h)" --no-merges | grep -E "^- feat" | linkify || true) |
| 63 | FIXES=$(git log $RANGE --pretty=format:"- %s (%h)" --no-merges | grep -E "^- fix" | linkify || true) |
| 64 | OTHER=$(git log $RANGE --pretty=format:"- %s (%h)" --no-merges | grep -Ev "^- (feat|fix)" | linkify || true) |
| 65 | |
| 66 | # Resolve the actual GitHub username per commit via the API instead of |
| 67 | # guessing from the freeform display name, so mentions tag the right account. |
| 68 | declare -A seen |
| 69 | CREDITS="" |
| 70 | while IFS=' ' read -r sha email name; do |
| 71 | [[ "$email" == *"[bot]"* || "$name" == *"[bot]"* || "$email" == *"github-actions"* ]] && continue |
| 72 | [[ -n "${seen[$email]:-}" ]] && continue |
| 73 | seen[$email]=1 |
| 74 | login=$(gh api "repos/${REPO}/commits/${sha}" --jq '.author.login // empty' 2>/dev/null || true) |
| 75 | CREDITS="${CREDITS}${login:-$name}"$'\n' |
| 76 | done < <(git log $RANGE --pretty=format:"%H %ae %aN" --no-merges) |
| 77 | CREDITS=$(printf '%s' "$CREDITS" | sed '/^$/d' | sort -u | sed 's|.*|[@&](https://github.com/&)|') |
| 78 | |
| 79 | FEATURES=$(truncate_lines "$FEATURES") |
| 80 | FIXES=$(truncate_lines "$FIXES") |
| 81 | OTHER=$(truncate_lines "$OTHER") |
| 82 | CREDITS=$(truncate_lines "$CREDITS") |
| 83 | |
| 84 | CHANGELOG="" |
| 85 | if [ -n "$FEATURES" ]; then |
| 86 | CHANGELOG="${CHANGELOG}### Features\n${FEATURES}\n" |
| 87 | fi |
| 88 | if [ -n "$FIXES" ]; then |
| 89 | CHANGELOG="${CHANGELOG}### Bug Fixes\n${FIXES}\n" |
| 90 | fi |
| 91 | if [ -n "$OTHER" ]; then |
| 92 | CHANGELOG="${CHANGELOG}### Other Changes\n${OTHER}\n" |
| 93 | fi |
| 94 | if [ -n "$CREDITS" ]; then |
| 95 | CHANGELOG="${CHANGELOG}### Credits\n${CREDITS}" |
| 96 | fi |
| 97 | |
| 98 | if [ -z "$CHANGELOG" ]; then |
| 99 | CHANGELOG="No notable changes." |
| 100 | fi |
| 101 | |
| 102 | if [ "$IS_PRERELEASE" = "true" ]; then |
| 103 | ROLE_ID="1520757652223819871" |
| 104 | TITLE="## 🚧 [${TAG}](${RELEASE_URL})" |
| 105 | else |
| 106 | ROLE_ID="1520472105836347515" |
| 107 | TITLE="## [${TAG}](${RELEASE_URL})" |
| 108 | fi |
| 109 | |
| 110 | PAYLOAD=$(jq -n \ |
| 111 | --arg ping "<@&${ROLE_ID}> ${TAG}" \ |
| 112 | --arg title "$TITLE" \ |
| 113 | --arg changelog "$(printf '%b' "$CHANGELOG")" \ |
| 114 | --arg url "$RELEASE_URL" \ |
| 115 | '{ |
| 116 | flags: 32768, |
| 117 | components: [ |
| 118 | { type: 10, content: $ping }, |
| 119 | { |
| 120 | type: 17, |
| 121 | spoiler: false, |
| 122 | components: [ |
| 123 | { |
| 124 | type: 9, |
| 125 | components: [ |
| 126 | { type: 10, content: $title } |
| 127 | ], |
| 128 | accessory: { |
| 129 | type: 2, |
| 130 | style: 5, |
| 131 | label: "Changelog", |
| 132 | url: $url |
| 133 | } |
| 134 | }, |
| 135 | { type: 14, divider: true, spacing: 1 }, |
| 136 | { type: 10, content: $changelog } |
| 137 | ] |
| 138 | } |
| 139 | ] |
| 140 | }') |
| 141 | |
| 142 | curl -sS -X POST \ |
| 143 | -H "Content-Type: application/json" \ |
| 144 | -d "$PAYLOAD" \ |
| 145 | "${DISCORD_WEBHOOK_URL}?with_components=true" |