name: Changelog Prose on: release: types: [published] # claude-code-action does not support the `release` event, so the dispatch # job relays core releases into a workflow_dispatch run. Manual dispatch # also lets us backfill prose for a release that was missed. workflow_dispatch: inputs: tag_name: description: 'Release tag (e.g. @videojs/core@10.0.0-beta.24)' required: true type: string release_url: description: 'Release URL (defaults to the tag release page)' required: false type: string concurrency: group: changelog-prose-${{ github.event.release.tag_name || inputs.tag_name }} cancel-in-progress: true jobs: dispatch: if: github.event_name == 'release' && startsWith(github.event.release.tag_name, '@videojs/core@') runs-on: ubuntu-latest permissions: actions: write steps: - name: Relay release to workflow_dispatch env: GH_TOKEN: ${{ github.token }} TAG_NAME: ${{ github.event.release.tag_name }} RELEASE_URL: ${{ github.event.release.html_url }} run: | gh workflow run changelog-prose.yml \ --repo "$GITHUB_REPOSITORY" \ --field tag_name="$TAG_NAME" \ --field release_url="$RELEASE_URL" prose: if: github.event_name == 'workflow_dispatch' && startsWith(inputs.tag_name, '@videojs/core@') runs-on: ubuntu-latest permissions: actions: read contents: write pull-requests: write issues: read steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 1 - name: Parse version id: version env: TAG_NAME: ${{ inputs.tag_name }} RELEASE_URL: ${{ inputs.release_url }} run: | VERSION="${TAG_NAME#@videojs/core@}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" if [ -n "$RELEASE_URL" ]; then echo "release_url=$RELEASE_URL" >> "$GITHUB_OUTPUT" else ENCODED_TAG=$(jq -rn --arg tag "$TAG_NAME" '$tag | @uri') echo "release_url=https://github.com/${GITHUB_REPOSITORY}/releases/tag/${ENCODED_TAG}" >> "$GITHUB_OUTPUT" fi - name: Guard — check raw changelog exists id: guard run: | FILE="site/src/content/changelog/${{ steps.version.outputs.version }}.md" if [ ! -f "$FILE" ]; then echo "::warning::Raw changelog file $FILE not found on main — skipping prose generation" echo "skip=true" >> "$GITHUB_OUTPUT" fi - name: Generate changelog prose if: steps.guard.outputs.skip != 'true' uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} claude_args: | --model sonnet --max-turns 20 --allowedTools "Bash(gh:*)" "Bash(git:*)" "Read" "Edit" "Write" "Glob" "Grep" prompt: | You are the changelog prose writer for Video.js 10. Version: ${{ steps.version.outputs.version }} Release URL: ${{ steps.version.outputs.release_url }} Your task is to rewrite a raw changelog file into polished narrative prose, then open a PR with the result. Steps: 1. **Load context:** - Read `.claude/skills/docs/references/writing-style.md` for tone and style rules. - Read the raw changelog at `site/src/content/changelog/${{ steps.version.outputs.version }}.md`. - Read other existing `.md` files in `site/src/content/changelog/` (if any) to match their tone and format. 2. **Gather PR context:** - Extract all PR numbers (e.g., `#906`) from the raw changelog body. - Build a single batched GraphQL query using `gh api graphql` with aliases to fetch all PRs at once: ``` { repository(owner: "videojs", name: "v10") { pr906: pullRequest(number: 906) { title body closingIssuesReferences(first: 5) { nodes { number title body parent { number title } } } } ... } } ``` - This gives you PR title, body, linked issues (with body), and parent epics in one API call. - Some PR numbers may resolve to null — skip those gracefully. 3. **Rewrite the changelog body:** - Replace the raw bullet list with narrative prose. Length scales with content: substantial releases get 100–300 words; small releases (a few bullets) get 1–3 honest sentences. Never pad to hit a word count. - Write paragraphs only: no headings, bullet lists, or tables. Use backticks for identifiers (e.g., `deps.alwaysBundle`, ``). - Lead with the 1–3 most impactful changes, informed by epic/issue context. Vary the opening; do not default to "This release…". - Group related PRs into cohesive stories rather than listing them individually (e.g., a feature landing across core, html, and react is one story with three links). - Call out breaking changes explicitly with concrete migration guidance from the PR body (old name → new name, what to update). Do not bury them. - Summarize smaller fixes briefly, grouped. - Skip internal-only changes (CI, tooling, changelog maintenance) — unless the release is entirely internal, in which case describe it honestly in a sentence or two rather than leaving the body empty. - Preserve PR links as inline markdown links in the prose (e.g., [#906](url)). Every change you mention keeps its PR link. - Drop per-change author credits ("by @user") — the PR link carries attribution. - If the raw file has a "New Contributors" section, remove it and end the prose with one sentence thanking first-time contributors by name, linking their GitHub profiles. - Follow `writing-style.md` strictly. - Do not fabricate — stick to what the PRs actually say. 4. **Write the `description` field:** - One concise sentence starting with a verb, summarizing the release for SEO/RSS (e.g., "Adds a hotkey system, gestures, and the mux-audio element."). Keep it under 140 characters. - Update the `description: ""` field in the file's YAML frontmatter. Change nothing else in the frontmatter. 5. **Create a PR:** - Create branch: `docs/changelog-prose-${{ steps.version.outputs.version }}` - Stage and commit the changed file: `docs(site): add changelog prose for ${{ steps.version.outputs.version }}` (commitlint's scope-enum has no `changelog` scope, so use `site`) - Push the branch and open a PR targeting `main`. - Include a link to the release in the PR body. - Keep the PR description concise.