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

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.