Skip to content

Commit 3a8ee52

Browse files
Wauplinclaude
andauthored
Add live Slack notifications to release workflow (#4046)
* Add live Slack notifications to release workflow Post a single message to Slack when a release starts, then update it with threaded replies as each sub-job completes (PyPI publish, release notes, downstream testing, post-release PR, CLI skill sync). A final job updates the original message with overall success/failure status and adds an emoji reaction. Each thread reply includes relevant links (PyPI history, GitHub releases, downstream compare URLs, PR links). The Slack announcement file is uploaded as a thread attachment. All Slack steps use continue-on-error so failures never block the release. Notifications are skipped during dry runs. Requires: SLACK_BOT_TOKEN secret (chat:write, reactions:write, files:write scopes) and SLACK_CHANNEL_ID repository variable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * emojis * remove rocket reaction * Fix slack-complete: add prepare to failure check, add continue-on-error - Include needs.prepare.result in the failure detection loop. If prepare fails after posting the Slack message (e.g. during git push), message_ts is set but nothing was published — without this check, the final message would incorrectly report success. - Add continue-on-error: true to match every other Slack step, so a Slack API failure doesn't cause the workflow to report as failed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix Slack message newlines: use $'\n' instead of literal \n In bash double quotes, \n is a literal backslash + n, not a newline. When passed through jq --arg, Slack receives the two-character string \n instead of a newline, breaking the block quote rendering. Use $'\n' concatenation to inject a real newline character. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1e34b33 commit 3a8ee52

1 file changed

Lines changed: 300 additions & 3 deletions

File tree

.github/workflows/release.yml

Lines changed: 300 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
# - APP_SECRET_PEM_HUGGINGFACE_HUB_BOT : GitHub App private key for huggingface-hub-bot
3030
# - APP_ID_HUB_SKILLS_REPO : GitHub App ID for creating tokens to access the skills repo
3131
# - APP_SECRET_PREM_HUB_SKILLS_REPO : GitHub App private key for the skills repo
32-
#
32+
# - SLACK_BOT_TOKEN : Slack Bot User OAuth Token (scopes: chat:write, reactions:write, files:write)
33+
#
34+
# Required repository variables:
35+
# - SLACK_CHANNEL_ID : Slack channel ID for release notifications
36+
#
3337
# An additional secret for OpenCode authentication is needed for automated release notes generation
3438
# (see the release-notes job below).
3539

@@ -72,6 +76,7 @@ jobs:
7276
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
7377
since_tag: ${{ steps.version.outputs.since_tag }}
7478
bump_type: ${{ steps.version.outputs.bump_type }}
79+
message_ts: ${{ steps.slack.outputs.message_ts }}
7580
steps:
7681
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
7782
with:
@@ -187,6 +192,41 @@ jobs:
187192
echo "| Prerelease | \`$IS_PRERELEASE\` |" >> $GITHUB_STEP_SUMMARY
188193
echo "| Dry run | \`${{ inputs.dry_run }}\` |" >> $GITHUB_STEP_SUMMARY
189194
195+
- name: Notify Slack — release started
196+
if: ${{ !inputs.dry_run }}
197+
id: slack
198+
continue-on-error: true
199+
env:
200+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
201+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
202+
run: |
203+
VERSION="${{ steps.version.outputs.version }}"
204+
TAG="${{ steps.version.outputs.tag }}"
205+
BRANCH="${{ steps.version.outputs.branch }}"
206+
RELEASE_TYPE="${{ inputs.release_type }}"
207+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
208+
209+
case "$RELEASE_TYPE" in
210+
minor-prerelease) TYPE_EMOJI="🏗️" TYPE_LABEL="Pre-release" ;;
211+
minor-release) TYPE_EMOJI="🚢" TYPE_LABEL="Release" ;;
212+
patch-release) TYPE_EMOJI="🐛" TYPE_LABEL="Patch" ;;
213+
esac
214+
215+
TEXT="${TYPE_EMOJI} *${TYPE_LABEL} \`${TAG}\` started*"$'\n'">Branch: \`${BRANCH}\` • <${RUN_URL}|View run>"
216+
217+
RESPONSE=$(curl -s -X POST https://slack.com/api/chat.postMessage \
218+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
219+
-H "Content-Type: application/json" \
220+
-d "$(jq -n \
221+
--arg channel "$SLACK_CHANNEL_ID" \
222+
--arg text "$TEXT" \
223+
'{channel: $channel, text: $text, unfurl_links: false}')")
224+
225+
TS=$(echo "$RESPONSE" | jq -r '.ts // empty')
226+
if [[ -n "$TS" ]]; then
227+
echo "message_ts=$TS" >> $GITHUB_OUTPUT
228+
fi
229+
190230
- name: Create or checkout release branch
191231
run: |
192232
BRANCH="${{ steps.version.outputs.branch }}"
@@ -264,6 +304,27 @@ jobs:
264304
env:
265305
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_DIST_HUGGINGFACE_HUB }}
266306

307+
- name: Notify Slack
308+
if: ${{ always() && needs.prepare.outputs.message_ts }}
309+
continue-on-error: true
310+
env:
311+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
312+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
313+
run: |
314+
if [[ "${{ job.status }}" == "success" ]]; then
315+
TEXT="✅ *PyPI* — \`huggingface-hub ${{ needs.prepare.outputs.version }}\` <https://pypi.org/project/huggingface-hub/#history|published>"
316+
else
317+
TEXT="❌ *PyPI* — \`huggingface-hub\` publish failed"
318+
fi
319+
curl -s -X POST https://slack.com/api/chat.postMessage \
320+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
321+
-H "Content-Type: application/json" \
322+
-d "$(jq -n \
323+
--arg channel "$SLACK_CHANNEL_ID" \
324+
--arg text "$TEXT" \
325+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
326+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
327+
267328
# ============================================================
268329
# 3. PUBLISH — PyPI (hf CLI)
269330
# ============================================================
@@ -289,6 +350,27 @@ jobs:
289350
env:
290351
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_DIST_HF }}
291352

353+
- name: Notify Slack
354+
if: ${{ always() && needs.prepare.outputs.message_ts }}
355+
continue-on-error: true
356+
env:
357+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
358+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
359+
run: |
360+
if [[ "${{ job.status }}" == "success" ]]; then
361+
TEXT="✅ *PyPI* — \`hf (CLI) ${{ needs.prepare.outputs.version }}\` <https://pypi.org/project/hf/#history|published>"
362+
else
363+
TEXT="❌ *PyPI* — \`hf (CLI)\` publish failed"
364+
fi
365+
curl -s -X POST https://slack.com/api/chat.postMessage \
366+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
367+
-H "Content-Type: application/json" \
368+
-d "$(jq -n \
369+
--arg channel "$SLACK_CHANNEL_ID" \
370+
--arg text "$TEXT" \
371+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
372+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
373+
292374
# ============================================================
293375
# 4. RELEASE NOTES — manage GitHub releases
294376
#
@@ -430,6 +512,28 @@ jobs:
430512
--generate-notes
431513
fi
432514
515+
- name: Notify Slack
516+
if: ${{ always() && needs.prepare.outputs.message_ts }}
517+
continue-on-error: true
518+
env:
519+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
520+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
521+
run: |
522+
RELEASES_URL="${{ github.server_url }}/${{ github.repository }}/releases"
523+
if [[ "${{ job.status }}" == "success" ]]; then
524+
TEXT="✅ *Release notes* — <${RELEASES_URL}|draft created> (\`${{ inputs.release_type }}\`)"
525+
else
526+
TEXT="❌ *Release notes* — failed"
527+
fi
528+
curl -s -X POST https://slack.com/api/chat.postMessage \
529+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
530+
-H "Content-Type: application/json" \
531+
-d "$(jq -n \
532+
--arg channel "$SLACK_CHANNEL_ID" \
533+
--arg text "$TEXT" \
534+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
535+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
536+
433537
# ============================================================
434538
# 4b. SLACK MESSAGE — generate Slack announcement for prereleases
435539
# ============================================================
@@ -502,6 +606,55 @@ jobs:
502606
cat ".release-notes/SLACK_MESSAGE_v${BASE_VERSION}.md" >> $GITHUB_STEP_SUMMARY
503607
echo '```' >> $GITHUB_STEP_SUMMARY
504608
609+
- name: Notify Slack
610+
if: ${{ always() && needs.prepare.outputs.message_ts }}
611+
continue-on-error: true
612+
env:
613+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
614+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
615+
run: |
616+
VERSION="${{ needs.prepare.outputs.version }}"
617+
BASE_VERSION=$(echo "$VERSION" | sed -E 's/\.rc.*//')
618+
SLACK_MSG_FILE=".release-notes/SLACK_MESSAGE_v${BASE_VERSION}.md"
619+
620+
if [[ "${{ job.status }}" == "success" ]]; then
621+
TEXT="✅ *Slack announcement* — message generated"
622+
else
623+
TEXT="❌ *Slack announcement* — generation failed"
624+
fi
625+
curl -s -X POST https://slack.com/api/chat.postMessage \
626+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
627+
-H "Content-Type: application/json" \
628+
-d "$(jq -n \
629+
--arg channel "$SLACK_CHANNEL_ID" \
630+
--arg text "$TEXT" \
631+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
632+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
633+
634+
# Upload the generated Slack announcement as a file in the thread
635+
if [[ -f "$SLACK_MSG_FILE" ]]; then
636+
FILE_SIZE=$(wc -c < "$SLACK_MSG_FILE")
637+
UPLOAD_RESP=$(curl -s -X POST https://slack.com/api/files.getUploadURLExternal \
638+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
639+
-H "Content-Type: application/x-www-form-urlencoded" \
640+
-d "filename=slack_announcement_v${BASE_VERSION}.md&length=${FILE_SIZE}")
641+
642+
UPLOAD_URL=$(echo "$UPLOAD_RESP" | jq -r '.upload_url // empty')
643+
FILE_ID=$(echo "$UPLOAD_RESP" | jq -r '.file_id // empty')
644+
645+
if [[ -n "$UPLOAD_URL" && -n "$FILE_ID" ]]; then
646+
curl -s -X POST "$UPLOAD_URL" -F "file=@${SLACK_MSG_FILE}" > /dev/null
647+
curl -s -X POST https://slack.com/api/files.completeUploadExternal \
648+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
649+
-H "Content-Type: application/json" \
650+
-d "$(jq -n \
651+
--arg file_id "$FILE_ID" \
652+
--arg channel "$SLACK_CHANNEL_ID" \
653+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
654+
'{files: [{id: $file_id}], channel_id: $channel, thread_ts: $thread_ts}')" > /dev/null
655+
fi
656+
fi
657+
505658
# ============================================================
506659
# 5. DOWNSTREAM TESTING — test RC in transformers, datasets, etc.
507660
# ============================================================
@@ -593,6 +746,31 @@ jobs:
593746
echo "- [Actions](https://github.com/huggingface/${{ matrix.target-repo }}/actions)" >> $GITHUB_STEP_SUMMARY
594747
echo "- [Compare](https://github.com/huggingface/${{ matrix.target-repo }}/compare/main...${BRANCH_NAME})" >> $GITHUB_STEP_SUMMARY
595748
749+
- name: Notify Slack
750+
if: ${{ always() && needs.prepare.outputs.message_ts }}
751+
continue-on-error: true
752+
env:
753+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
754+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
755+
run: |
756+
REPO="${{ matrix.target-repo }}"
757+
VERSION="${{ needs.prepare.outputs.version }}"
758+
BRANCH_NAME="ci-test-huggingface-hub-${VERSION}-release"
759+
COMPARE_URL="https://github.com/huggingface/${REPO}/compare/main...${BRANCH_NAME}"
760+
if [[ "${{ job.status }}" == "success" ]]; then
761+
TEXT="✅ *Downstream* — \`${REPO}\` <${COMPARE_URL}|test branch pushed>"
762+
else
763+
TEXT="❌ *Downstream* — \`${REPO}\` failed"
764+
fi
765+
curl -s -X POST https://slack.com/api/chat.postMessage \
766+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
767+
-H "Content-Type: application/json" \
768+
-d "$(jq -n \
769+
--arg channel "$SLACK_CHANNEL_ID" \
770+
--arg text "$TEXT" \
771+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
772+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
773+
596774
# ============================================================
597775
# 6. POST-RELEASE — open PR to bump main to next dev version
598776
# ============================================================
@@ -621,6 +799,7 @@ jobs:
621799
git config user.email "huggingface-hub-bot[bot]@users.noreply.github.com"
622800
623801
- name: Create version bump PR
802+
id: bump_pr
624803
env:
625804
GH_TOKEN: ${{ steps.app_token.outputs.token }}
626805
run: |
@@ -646,10 +825,35 @@ jobs:
646825
git commit -m "Post-release: bump version to ${DEV_VERSION}"
647826
git push -u origin "$PR_BRANCH"
648827
649-
gh pr create \
828+
PR_URL=$(gh pr create \
650829
--title "Post-release: bump version to ${DEV_VERSION}" \
651830
--body "Automated version bump after v${VERSION} release." \
652-
--base main
831+
--base main)
832+
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
833+
834+
- name: Notify Slack
835+
if: ${{ always() && needs.prepare.outputs.message_ts }}
836+
continue-on-error: true
837+
env:
838+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
839+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
840+
run: |
841+
PR_URL="${{ steps.bump_pr.outputs.pr_url }}"
842+
if [[ "${{ job.status }}" == "success" && -n "$PR_URL" ]]; then
843+
TEXT="✅ *Post-release* — <${PR_URL}|version bump PR> opened"
844+
elif [[ "${{ job.status }}" == "success" ]]; then
845+
TEXT="✅ *Post-release* — skipped (version already bumped)"
846+
else
847+
TEXT="❌ *Post-release* — failed"
848+
fi
849+
curl -s -X POST https://slack.com/api/chat.postMessage \
850+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
851+
-H "Content-Type: application/json" \
852+
-d "$(jq -n \
853+
--arg channel "$SLACK_CHANNEL_ID" \
854+
--arg text "$TEXT" \
855+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
856+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
653857
654858
# ============================================================
655859
# 7. SYNC HF CLI SKILL — update skill docs in skills repo
@@ -724,3 +928,96 @@ jobs:
724928
labels: |
725929
automated
726930
cli-sync
931+
932+
- name: Notify Slack
933+
if: ${{ always() && needs.prepare.outputs.message_ts }}
934+
continue-on-error: true
935+
env:
936+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
937+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
938+
run: |
939+
if [[ "${{ job.status }}" == "success" ]]; then
940+
TEXT="✅ *CLI skill sync* — done"
941+
else
942+
TEXT="❌ *CLI skill sync* — failed"
943+
fi
944+
curl -s -X POST https://slack.com/api/chat.postMessage \
945+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
946+
-H "Content-Type: application/json" \
947+
-d "$(jq -n \
948+
--arg channel "$SLACK_CHANNEL_ID" \
949+
--arg text "$TEXT" \
950+
--arg thread_ts "${{ needs.prepare.outputs.message_ts }}" \
951+
'{channel: $channel, text: $text, thread_ts: $thread_ts, unfurl_links: false}')" > /dev/null
952+
953+
# ============================================================
954+
# 8. SLACK — final status update
955+
# ============================================================
956+
slack-complete:
957+
needs: [prepare, publish-pypi, publish-hf-cli, release-notes, slack-message, test-downstream, post-release, sync-hf-cli-skill]
958+
if: ${{ always() && !inputs.dry_run && needs.prepare.outputs.message_ts }}
959+
runs-on: ubuntu-latest
960+
steps:
961+
- name: Update Slack message with final status
962+
continue-on-error: true
963+
env:
964+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
965+
SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }}
966+
run: |
967+
MESSAGE_TS="${{ needs.prepare.outputs.message_ts }}"
968+
TAG="${{ needs.prepare.outputs.tag }}"
969+
BRANCH="${{ needs.prepare.outputs.branch }}"
970+
RELEASE_TYPE="${{ inputs.release_type }}"
971+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
972+
973+
# Check if any job failed or was cancelled (including prepare itself,
974+
# which can fail after posting the Slack message, e.g. during git push)
975+
FAILED=false
976+
for result in \
977+
"${{ needs.prepare.result }}" \
978+
"${{ needs.publish-pypi.result }}" \
979+
"${{ needs.publish-hf-cli.result }}" \
980+
"${{ needs.release-notes.result }}" \
981+
"${{ needs.slack-message.result }}" \
982+
"${{ needs.test-downstream.result }}" \
983+
"${{ needs.post-release.result }}" \
984+
"${{ needs.sync-hf-cli-skill.result }}"; do
985+
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
986+
FAILED=true
987+
break
988+
fi
989+
done
990+
991+
case "$RELEASE_TYPE" in
992+
minor-prerelease) TYPE_LABEL="Pre-release" ;;
993+
minor-release) TYPE_LABEL="Release" ;;
994+
patch-release) TYPE_LABEL="Patch" ;;
995+
esac
996+
997+
if [[ "$FAILED" == "true" ]]; then
998+
TEXT="❌ *${TYPE_LABEL} \`${TAG}\` failed*"$'\n'">Branch: \`${BRANCH}\` • <${RUN_URL}|View run>"
999+
REACTION="x"
1000+
else
1001+
TEXT="✅ *${TYPE_LABEL} \`${TAG}\` completed*"$'\n'">Branch: \`${BRANCH}\` • <${RUN_URL}|View run>"
1002+
REACTION="white_check_mark"
1003+
fi
1004+
1005+
# Update the original message
1006+
curl -s -X POST https://slack.com/api/chat.update \
1007+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
1008+
-H "Content-Type: application/json" \
1009+
-d "$(jq -n \
1010+
--arg channel "$SLACK_CHANNEL_ID" \
1011+
--arg text "$TEXT" \
1012+
--arg ts "$MESSAGE_TS" \
1013+
'{channel: $channel, text: $text, ts: $ts}')" > /dev/null
1014+
1015+
# Add status reaction
1016+
curl -s -X POST https://slack.com/api/reactions.add \
1017+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
1018+
-H "Content-Type: application/json" \
1019+
-d "$(jq -n \
1020+
--arg channel "$SLACK_CHANNEL_ID" \
1021+
--arg name "$REACTION" \
1022+
--arg timestamp "$MESSAGE_TS" \
1023+
'{channel: $channel, name: $name, timestamp: $timestamp}')" > /dev/null

0 commit comments

Comments
 (0)