GitHub Actions TTS: narrate release notes in CI — Audra
Narrating release notes in CI is the canonical first GitHub Actions + TTS job: on every tag, render the changelog as audio and attach it to the release. Everything needed lives in a single workflow file — no custom action, no Docker image, just curl and the Actions primitives.
The workflow
name: release-narration
on:
push:
tags: ["v*"]
permissions:
contents: write # attach asset to the release
jobs:
narrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # git log needs full history
- name: Render changelog audio
env:
AUDRA_KEY: ${{ secrets.AUDRA_API_KEY }}
run: |
git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD > notes.txt
curl https://audratalks.com/v2/speech \
-H "Authorization: Bearer $AUDRA_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $GITHUB_SHA" \
-d "$(jq -Rs --arg v "$GITHUB_REF_NAME" \
'{model:"audra-essential", voice:"june", format:"mp3",
text: "Release " + $v + ".\n" + .}' < notes.txt)" \
--output release.mp3
- name: Attach to release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$GITHUB_REF_NAME" release.mp3 --clobber
Why each piece is there
- secrets.AUDRA_API_KEY: stored under Settings > Secrets > Actions. Never echoed; the workflow only ever references it via env.
- Idempotency-Key: $GITHUB_SHA — a re-run of a failed job retries the same logical render without a second charge. POST /v2/speech replays the same audio when the key matches.
- jq -Rs: reads the changelog raw and JSON-escapes it, so newlines and quotes in commit subjects cannot break the body. This is the one step hand-rolled JSON always gets wrong.
- --clobber: re-uploads overwrite the asset on job re-runs instead of failing the step.
CI-specific limits to plan around
Public-repo runners get 2,000 free minutes/month; the render step above runs ~2–4 wall-clock minutes for a typical changelog, so cost is dominated by your Audra characters ($25/1M on Essential), not Actions minutes. Free-key rate limits (60 RPM) are irrelevant on tag pushes, but if you narrate every push on a busy repo, add a concurrency group — concurrency: group: narration-${{ github.ref }} with cancel-in-progress: false — so overlapping runs queue instead of colliding. For narrating full documentation (not just tags), the docs-site pipeline at /blog/documentation-tts-api shows the chunking approach; the jobs API at /blog/tts-api-async-jobs-webhooks handles scripts past 50k characters.