Skip to content

Commit 5d6537c

Browse files
author
David Elner
committed
Added: Release actions
1 parent e2d24d2 commit 5d6537c

5 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Create GitHub Release
2+
description: >
3+
Creates a GitHub release with generated release notes.
4+
In dry run mode, previews the release notes without creating anything.
5+
6+
inputs:
7+
release_tag:
8+
description: "The release tag (e.g. v0.3.2)"
9+
required: true
10+
sha:
11+
description: "The commit SHA the release points to"
12+
required: true
13+
notes:
14+
description: "Base64-encoded release notes body from prepare action"
15+
required: false
16+
default: ''
17+
dry_run:
18+
description: "Preview release notes without creating the release"
19+
required: false
20+
default: 'false'
21+
22+
runs:
23+
using: composite
24+
steps:
25+
- name: Create GitHub release
26+
if: ${{ inputs.dry_run == 'false' }}
27+
shell: bash
28+
env:
29+
GITHUB_TOKEN: ${{ github.token }}
30+
TAG: ${{ inputs.release_tag }}
31+
run: |
32+
echo "${{ inputs.notes }}" | base64 -d 2>/dev/null > /tmp/release-notes.md
33+
gh release create "$TAG" \
34+
--title "$TAG" \
35+
--notes-file /tmp/release-notes.md \
36+
--target "${{ inputs.sha }}"
37+
38+
- name: Release notes preview
39+
if: ${{ inputs.dry_run == 'true' }}
40+
shell: bash
41+
run: |
42+
echo "DRY RUN: would create GitHub release ${{ inputs.release_tag }}"
43+
echo "--- Release notes preview ---"
44+
echo "${{ inputs.notes }}" | base64 -d 2>/dev/null || echo "(unavailable)"
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
name: Notify Release Pending
2+
description: >
3+
Posts the pre-approval job summary and Slack notification.
4+
Called before the environment gate — reviewer sees this before approving.
5+
6+
inputs:
7+
sha:
8+
description: "Commit SHA being released"
9+
required: true
10+
release_tag:
11+
description: "The release tag (e.g. v0.3.2)"
12+
required: true
13+
prev_tag:
14+
description: "Previous release tag"
15+
required: false
16+
default: ''
17+
branch:
18+
description: "Branch containing the SHA"
19+
required: true
20+
on_main:
21+
description: "Whether the SHA is on the main branch"
22+
required: true
23+
commit_message:
24+
description: "Commit message at the SHA"
25+
required: true
26+
pr_list:
27+
description: "x1f-delimited PR list from prepare action"
28+
required: false
29+
default: ''
30+
notes:
31+
description: "Base64-encoded release notes from prepare action"
32+
required: false
33+
default: ''
34+
dry_run:
35+
description: "Whether this is a dry run"
36+
required: false
37+
default: 'false'
38+
slack_token:
39+
description: "Slack bot token for posting messages"
40+
required: true
41+
slack_channel:
42+
description: "Slack channel ID to post to"
43+
required: true
44+
emoji:
45+
description: "Emoji prefix for Slack messages"
46+
required: false
47+
default: ':package:'
48+
49+
runs:
50+
using: composite
51+
steps:
52+
- name: Post release summary
53+
shell: bash
54+
env:
55+
TAG: ${{ inputs.release_tag }}
56+
run: |
57+
NOTES=$(echo "${{ inputs.notes }}" | base64 -d 2>/dev/null)
58+
if [ -z "$NOTES" ]; then NOTES="_Release notes unavailable._"; fi
59+
60+
BRANCH_LABEL="[${{ inputs.branch }}]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/${{ inputs.branch }})"
61+
if [ "${{ inputs.on_main }}" = "false" ]; then
62+
BRANCH_LABEL="$BRANCH_LABEL ⚠️"
63+
fi
64+
65+
echo "## $GITHUB_REPOSITORY $TAG" >> $GITHUB_STEP_SUMMARY
66+
echo "" >> $GITHUB_STEP_SUMMARY
67+
68+
if [ "${{ inputs.dry_run }}" = "true" ]; then
69+
echo "> [!NOTE]" >> $GITHUB_STEP_SUMMARY
70+
echo "> Dry run: Nothing will be tagged or published." >> $GITHUB_STEP_SUMMARY
71+
echo "" >> $GITHUB_STEP_SUMMARY
72+
fi
73+
74+
if [ "${{ inputs.on_main }}" = "false" ]; then
75+
echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY
76+
echo "> Release SHA is not on main: Is this a special release? (e.g. beta, backport, etc)" >> $GITHUB_STEP_SUMMARY
77+
echo "" >> $GITHUB_STEP_SUMMARY
78+
fi
79+
80+
echo "**SHA:** [${{ inputs.sha }}]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/${{ inputs.sha }})" >> $GITHUB_STEP_SUMMARY
81+
echo "**Commit:** ${{ inputs.commit_message }}" >> $GITHUB_STEP_SUMMARY
82+
echo "**Branch:** $BRANCH_LABEL" >> $GITHUB_STEP_SUMMARY
83+
84+
if [ -n "${{ inputs.prev_tag }}" ]; then
85+
DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${{ inputs.prev_tag }}...${{ inputs.sha }}"
86+
echo "**Diff:** [${{ inputs.prev_tag }}...$TAG]($DIFF_URL)" >> $GITHUB_STEP_SUMMARY
87+
fi
88+
89+
echo "" >> $GITHUB_STEP_SUMMARY
90+
echo "$NOTES" >> $GITHUB_STEP_SUMMARY
91+
92+
- name: Notify Slack
93+
shell: bash
94+
env:
95+
SLACK_BOT_TOKEN: ${{ inputs.slack_token }}
96+
SLACK_CHANNEL: ${{ inputs.slack_channel }}
97+
TAG: ${{ inputs.release_tag }}
98+
APPROVE_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
99+
run: |
100+
BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/${{ inputs.branch }}"
101+
BRANCH_LINK="<$BRANCH_URL|${{ inputs.branch }}>"
102+
if [ "${{ inputs.on_main }}" = "false" ]; then
103+
BRANCH_INFO="> ⚠️ NOT on main: $BRANCH_LINK"
104+
else
105+
BRANCH_INFO="$BRANCH_LINK"
106+
fi
107+
108+
DIFF_PART=""
109+
if [ -n "${{ inputs.prev_tag }}" ]; then
110+
DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${{ inputs.prev_tag }}...${{ inputs.sha }}"
111+
DIFF_PART=" · <$DIFF_URL|${{ inputs.prev_tag }}...$TAG>"
112+
fi
113+
114+
PR_LIST=$(echo "${{ inputs.pr_list }}" | tr $'\x1f' '\n' | sed '/^$/d')
115+
116+
TEXT="${{ inputs.emoji }} *$GITHUB_REPOSITORY $TAG* awaiting approval"
117+
118+
if [ "${{ inputs.dry_run }}" = "true" ]; then
119+
TEXT="$TEXT\n> :information_source: _Dry run: nothing will be tagged or published._"
120+
fi
121+
122+
TEXT="$TEXT\n${BRANCH_INFO}${DIFF_PART}"
123+
124+
if [ -n "$PR_LIST" ]; then
125+
TEXT="$TEXT\n$PR_LIST"
126+
fi
127+
128+
TEXT="$TEXT\n<$APPROVE_URL|View changes & approve>"
129+
130+
# jq --arg passes strings literally, so \n must be real newlines before encoding
131+
TEXT=$(printf '%b' "$TEXT")
132+
curl -s -X POST "https://slack.com/api/chat.postMessage" \
133+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
134+
-H "Content-Type: application/json; charset=utf-8" \
135+
-d "$(jq -n --arg channel "$SLACK_CHANNEL" --arg text "$TEXT" \
136+
'{channel: $channel, text: $text}')"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Notify Release Complete
2+
description: >
3+
Posts the post-publish Slack notification with release link and PR list.
4+
5+
inputs:
6+
release_tag:
7+
description: "The release tag (e.g. v0.3.2)"
8+
required: true
9+
prev_tag:
10+
description: "Previous release tag"
11+
required: false
12+
default: ''
13+
branch:
14+
description: "Branch the release was made from"
15+
required: true
16+
on_main:
17+
description: "Whether the release was from main"
18+
required: true
19+
pr_list:
20+
description: "x1f-delimited PR list from prepare action"
21+
required: false
22+
default: ''
23+
dry_run:
24+
description: "Whether this was a dry run"
25+
required: false
26+
default: 'false'
27+
slack_token:
28+
description: "Slack bot token for posting messages"
29+
required: true
30+
slack_channel:
31+
description: "Slack channel ID to post to"
32+
required: true
33+
emoji:
34+
description: "Emoji prefix for Slack messages"
35+
required: false
36+
default: ':package:'
37+
38+
runs:
39+
using: composite
40+
steps:
41+
- name: Notify Slack
42+
shell: bash
43+
env:
44+
SLACK_BOT_TOKEN: ${{ inputs.slack_token }}
45+
SLACK_CHANNEL: ${{ inputs.slack_channel }}
46+
TAG: ${{ inputs.release_tag }}
47+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
48+
run: |
49+
RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG"
50+
51+
DIFF_PART=""
52+
if [ -n "${{ inputs.prev_tag }}" ]; then
53+
DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${{ inputs.prev_tag }}...$TAG"
54+
DIFF_PART="<$DIFF_URL|${{ inputs.prev_tag }}...$TAG> · "
55+
fi
56+
57+
PR_LIST=$(echo "${{ inputs.pr_list }}" | tr $'\x1f' '\n' | sed '/^$/d')
58+
59+
if [ "${{ inputs.dry_run }}" = "true" ]; then
60+
TEXT="${{ inputs.emoji }} *$GITHUB_REPOSITORY $TAG* complete"
61+
TEXT="$TEXT\n> :information_source: _Dry run: gem built, nothing tagged or published._"
62+
else
63+
TEXT="${{ inputs.emoji }} *$GITHUB_REPOSITORY $TAG* published"
64+
fi
65+
66+
if [ "${{ inputs.on_main }}" = "false" ]; then
67+
BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/${{ inputs.branch }}"
68+
TEXT="$TEXT\n> ⚠️ NOT on main: <$BRANCH_URL|${{ inputs.branch }}>"
69+
fi
70+
71+
if [ "${{ inputs.dry_run }}" = "true" ]; then
72+
TEXT="$TEXT\n${DIFF_PART}<$RUN_URL|View dry run>"
73+
else
74+
TEXT="$TEXT\n${DIFF_PART}<$RELEASE_URL|View release>"
75+
fi
76+
77+
if [ -n "$PR_LIST" ]; then
78+
TEXT="$TEXT\n$PR_LIST"
79+
fi
80+
81+
# jq --arg passes strings literally, so \n must be real newlines before encoding
82+
TEXT=$(printf '%b' "$TEXT")
83+
curl -s -X POST "https://slack.com/api/chat.postMessage" \
84+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
85+
-H "Content-Type: application/json; charset=utf-8" \
86+
-d "$(jq -n --arg channel "$SLACK_CHANNEL" --arg text "$TEXT" \
87+
'{channel: $channel, text: $text}')"

actions/release/prepare/action.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Prepare Release
2+
description: >
3+
Fetches release notes and formats the PR list for notifications.
4+
Requires contents:write permission on the calling job (for generate-notes API).
5+
6+
inputs:
7+
release_tag:
8+
description: "The release tag (e.g. v0.3.2)"
9+
required: true
10+
sha:
11+
description: "The commit SHA being released"
12+
required: true
13+
prev_tag:
14+
description: "The previous release tag"
15+
required: false
16+
default: ''
17+
18+
outputs:
19+
pr_list:
20+
description: "Formatted PR list (x1f-delimited for safe job output passing)"
21+
value: ${{ steps.fetch.outputs.pr_list }}
22+
notes:
23+
description: "Base64-encoded full release notes body"
24+
value: ${{ steps.fetch.outputs.notes }}
25+
26+
runs:
27+
using: composite
28+
steps:
29+
- name: Fetch PR list and release notes
30+
id: fetch
31+
shell: bash
32+
env:
33+
GH_TOKEN: ${{ github.token }}
34+
run: |
35+
PREV_TAG="${{ inputs.prev_tag }}"
36+
BODY=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
37+
--method POST \
38+
--field tag_name="${{ inputs.release_tag }}" \
39+
--field target_commitish="${{ inputs.sha }}" \
40+
${PREV_TAG:+--field previous_tag_name="$PREV_TAG"} \
41+
--jq '.body' 2>/dev/null || echo "")
42+
43+
PR_LIST=$(echo "$BODY" \
44+
| grep "^\* " \
45+
| grep -v "made their first contribution" \
46+
| grep -v "Full Changelog" \
47+
| head -10 \
48+
| sed 's|^\* ||' \
49+
| sed 's| by @[^ ]*||' \
50+
| sed 's@ in \(https://[^ ]*/pull/\([0-9]*\)\)@ (<\1|#\2>)@' \
51+
| sed 's/^/• /' \
52+
| tr '\n' $'\x1f' || echo "")
53+
54+
echo "pr_list=$PR_LIST" >> $GITHUB_OUTPUT
55+
echo "notes=$(echo "$BODY" | base64 -w 0)" >> $GITHUB_OUTPUT
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Validate Release
2+
description: >
3+
Validates a release SHA and computes release metadata.
4+
Requires the repository to be checked out at the target SHA.
5+
6+
inputs:
7+
version:
8+
description: "Version string read from the language-specific version file"
9+
required: true
10+
sha:
11+
description: "Commit SHA being released"
12+
required: true
13+
dry_run:
14+
description: "Skip tag existence failure in dry runs"
15+
required: false
16+
default: 'false'
17+
18+
outputs:
19+
release_tag:
20+
description: "The release tag (e.g. v0.3.2)"
21+
value: ${{ steps.validate.outputs.release_tag }}
22+
prev_tag:
23+
description: "The previous release tag"
24+
value: ${{ steps.validate.outputs.prev_tag }}
25+
branch:
26+
description: "The branch containing the SHA"
27+
value: ${{ steps.validate.outputs.branch }}
28+
on_main:
29+
description: "Whether the SHA is on the main branch"
30+
value: ${{ steps.validate.outputs.on_main }}
31+
commit_message:
32+
description: "The commit message at the SHA"
33+
value: ${{ steps.validate.outputs.commit_message }}
34+
35+
runs:
36+
using: composite
37+
steps:
38+
- name: Validate release
39+
id: validate
40+
shell: bash
41+
run: |
42+
TAG="v${{ inputs.version }}"
43+
COMMIT_MSG=$(git log -1 --format="%s" HEAD)
44+
BRANCH=$(git branch -r --contains HEAD --format="%(refname:short)" | sed 's|origin/||' | head -1)
45+
46+
if git rev-parse "$TAG" >/dev/null 2>&1; then
47+
if [ "${{ inputs.dry_run }}" = "true" ]; then
48+
echo "Warning: Tag $TAG already exists — skipping in dry run"
49+
else
50+
echo "Error: Tag $TAG already exists — has the version been bumped?"
51+
exit 1
52+
fi
53+
fi
54+
55+
if git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then
56+
ON_MAIN=true
57+
else
58+
ON_MAIN=false
59+
fi
60+
61+
PREV_TAG=$(git describe --tags --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*' HEAD^ 2>/dev/null || echo "")
62+
63+
echo "release_tag=$TAG" >> $GITHUB_OUTPUT
64+
echo "commit_message=$COMMIT_MSG" >> $GITHUB_OUTPUT
65+
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
66+
echo "on_main=$ON_MAIN" >> $GITHUB_OUTPUT
67+
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
68+
echo "Validated $TAG @ ${{ inputs.sha }} ($COMMIT_MSG)"

0 commit comments

Comments
 (0)