feat: add ai-release-notes composite action#132
feat: add ai-release-notes composite action#132simon (susishopware) wants to merge 2 commits intomainfrom
Conversation
Extract the AI release notes workflow from nexus-workflow-builder into a reusable composite action. The action generates release notes via the GitHub API, rewrites them with the GitHub Models API (GPT-4o), and optionally creates a draft GitHub release. All formatting options (bold, PR links, authors, sections) are configurable via inputs with sensible defaults that reproduce the current nexus-workflow-builder behaviour. Ref: shopware/pipe-fiction#477 Made-with: Cursor
Feature OverviewCore Functionality (extracted from
|
| Function | Status | Description |
|---|---|---|
| Tag detection | ✅ | Automatically detects the previous v* tag from Git history |
| Raw notes generation | ✅ | Uses the GitHub API (repos.generateReleaseNotes) as changelog base |
| AI rewrite | ✅ | Rewrites raw notes via GitHub Models API (GPT-4o) into polished release notes |
| Draft release | ✅ | Creates a draft GitHub release with the AI-generated notes |
Configurable Inputs
| Feature | Input | Default | Description |
|---|---|---|---|
| Product name | product-name |
(required) | Product name used in the AI prompt |
| Product context | product-description |
"" |
Optional context for better AI output |
| Bold formatting | bold-features |
true |
Bold feature names in bullets |
| PR links | include-pr-links |
false |
Show/hide PR links in bullets |
| Authors | include-authors |
false |
Show/hide author attributions |
| Sections | sections |
New Features,Improvements,Bug Fixes,Other |
Section order and selection |
| Collapse types | collapse-types |
dependency,translation,CI |
Change types collapsed under the last section |
| Additional rules | additional-rules |
"" |
Append extra rules to the default prompt |
| Full override | custom-prompt |
"" |
Replace the entire system prompt |
| AI model | model |
gpt-4o |
GitHub Models API model |
| Temperature | temperature |
0.4 |
Creativity level |
| Draft | draft |
true |
Create release as draft |
| Pre-release | prerelease |
false |
Mark as pre-release |
| Create release | create-release |
true |
false = generate notes only |
| Token | github-token |
github.token |
Requires models:read + contents:write |
Outputs
| Output | Description |
|---|---|
release-notes |
AI-generated release notes (Markdown) |
raw-notes |
Raw GitHub changelog (before AI rewrite) |
release-url |
URL of the created release |
previous-tag |
Detected previous git tag |
References
- Issue: shopware/pipe-fiction#477
- Original workflow:
nexus-workflow-builder/.github/workflows/ai-release-notes.yml - Next step after merge: PR in NWB to switch to the shared action
|
Note: The corresponding PR in nexus-workflow-builder that switches to this shared action is already prepared as a draft: shopware/nexus-workflow-builder#524 Merge order: this PR first → then NWB PR. |
ai-release-notes/action.yml
Outdated
| const productName = `${{ inputs.product-name }}`; | ||
| const productDesc = `${{ inputs.product-description }}`; | ||
| const sections = `${{ inputs.sections }}`; | ||
| const collapseTypes = `${{ inputs.collapse-types }}`; | ||
| const boldFeatures = `${{ inputs.bold-features }}` === 'true'; | ||
| const includePrLinks = `${{ inputs.include-pr-links }}` === 'true'; | ||
| const includeAuthors = `${{ inputs.include-authors }}` === 'true'; | ||
| const additionalRules = `${{ inputs.additional-rules }}`; |
There was a problem hiding this comment.
Doesn't this open it up to script injection?
The fix here is to pass all inputs through the env: and then read them via process.env.*, ie:
env:
INPUT_PRODUCT_NAME: "${{ inputs.product-name }}"
INPUT_CUSTOM_PROMPT: "${{ inputs.custom-prompt }}"
INPUT_DRAFT: "${{ inputs.draft }}"
# other vars
with:
script: |
# other code
const productName = process.env.INPUT_PRODUCT_NAME;
const isDraft = process.env.INPUT_DRAFT === 'true';
ai-release-notes/action.yml
Outdated
| } | ||
|
|
||
| const result = await response.json(); | ||
| const aiNotes = result.choices[0].message.content; |
There was a problem hiding this comment.
Perhaps a good idea to add a guard here just so we don't break when we find an unexpected response:
| const aiNotes = result.choices[0].message.content; | |
| const result = await response.json(); | |
| if (!result.choices?.length) { | |
| core.setFailed(`Unexpected API response: ${JSON.stringify(result)}`); | |
| return; | |
| } | |
| const aiNotes = result.choices[0].message.content; |
ai-release-notes/action.yml
Outdated
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| tag_name: tag, | ||
| name: `Release ${tag}`, |
There was a problem hiding this comment.
Release is hardcoded. Maybe make this configurable too?
ai-release-notes/action.yml
Outdated
| shell: bash | ||
| run: | | ||
| CURRENT_TAG="${GITHUB_REF_NAME}" | ||
| PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v' | grep -v "^${CURRENT_TAG}$" | head -n 1) |
There was a problem hiding this comment.
What happens when someone does release-1.1.0 or just 1.1.0 instead of v1.1.0?
A fix for this could be to add an extra input for the tag pattern:
tag-pattern:
description: "Regex pattern to match tags (e.g. '^v', '^release-')"
required: false
default: "^v"And then update this command to use it:
| PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v' | grep -v "^${CURRENT_TAG}$" | head -n 1) | |
| PREV_TAG=$(git tag --sort=-v:refname | grep -E "${TAG_PATTERN}" | grep -v "^${CURRENT_TAG}$" | head -n 1) |
ai-release-notes/action.yml
Outdated
| systemPrompt = `You are a release-notes editor for a SaaS product called ${productName}.`; | ||
|
|
||
| if (productDesc) { | ||
| systemPrompt += `\nProduct context: ${productDesc}`; | ||
| } | ||
|
|
||
| systemPrompt += ` | ||
| You receive the auto-generated GitHub "What's Changed" list and rewrite it into polished, | ||
| user-friendly release notes grouped by category. | ||
|
|
||
| Rules: | ||
| - Use these sections (omit any that have no items): ${sections}. | ||
| - Each bullet should be concise but descriptive — translate commit-speak into product language. | ||
| - Collapse ${collapseTypes} changes into a single bullet each under "${lastSection}".`; | ||
|
|
||
| if (boldFeatures) { | ||
| systemPrompt += `\n- Use **bold** for feature names in bullets.`; | ||
| } | ||
| if (!includePrLinks) { | ||
| systemPrompt += `\n- Do NOT include PR links or PR numbers in the bullets.`; | ||
| } | ||
| if (!includeAuthors) { | ||
| systemPrompt += `\n- Do NOT include author attributions in the bullets.`; | ||
| } | ||
|
|
||
| systemPrompt += ` | ||
| - Keep the "Full Changelog" comparison link at the bottom exactly as provided. | ||
| - Start with a heading: ## Release Notes — ${currentTag} | ||
| - Output clean Markdown, nothing else.`; | ||
|
|
||
| if (additionalRules) { | ||
| systemPrompt += `\n\nAdditional rules:\n${additionalRules}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
So something I didn't know or even consider, is that the yaml whitespace will be used in the prompt to the AI, which is not only inefficient, but also wastes tokens. A suggestion is to push to a variable:
| systemPrompt = `You are a release-notes editor for a SaaS product called ${productName}.`; | |
| if (productDesc) { | |
| systemPrompt += `\nProduct context: ${productDesc}`; | |
| } | |
| systemPrompt += ` | |
| You receive the auto-generated GitHub "What's Changed" list and rewrite it into polished, | |
| user-friendly release notes grouped by category. | |
| Rules: | |
| - Use these sections (omit any that have no items): ${sections}. | |
| - Each bullet should be concise but descriptive — translate commit-speak into product language. | |
| - Collapse ${collapseTypes} changes into a single bullet each under "${lastSection}".`; | |
| if (boldFeatures) { | |
| systemPrompt += `\n- Use **bold** for feature names in bullets.`; | |
| } | |
| if (!includePrLinks) { | |
| systemPrompt += `\n- Do NOT include PR links or PR numbers in the bullets.`; | |
| } | |
| if (!includeAuthors) { | |
| systemPrompt += `\n- Do NOT include author attributions in the bullets.`; | |
| } | |
| systemPrompt += ` | |
| - Keep the "Full Changelog" comparison link at the bottom exactly as provided. | |
| - Start with a heading: ## Release Notes — ${currentTag} | |
| - Output clean Markdown, nothing else.`; | |
| if (additionalRules) { | |
| systemPrompt += `\n\nAdditional rules:\n${additionalRules}`; | |
| } | |
| } | |
| const lines = [ | |
| `You are a release-notes editor for a SaaS product called ${productName}.`, | |
| ]; | |
| if (productDesc) { | |
| lines.push(`Product context: ${productDesc}`); | |
| } | |
| lines.push( | |
| 'You receive the auto-generated GitHub "What\'s Changed" list and rewrite it into polished, user-friendly release notes grouped by category.', | |
| '', | |
| 'Rules:', | |
| `- Use these sections (omit any that have no items): ${sections}.`, | |
| '- Each bullet should be concise but descriptive — translate commit-speak into product language.', | |
| `- Collapse ${collapseTypes} changes into a single bullet each under "${lastSection}".`, | |
| ); | |
| if (boldFeatures) { | |
| lines.push('- Use **bold** for feature names in bullets.'); | |
| } | |
| if (!includePrLinks) { | |
| lines.push('- Do NOT include PR links or PR numbers in the bullets.'); | |
| } | |
| if (!includeAuthors) { | |
| lines.push('- Do NOT include author attributions in the bullets.'); | |
| } | |
| lines.push( | |
| '- Keep the "Full Changelog" comparison link at the bottom exactly as provided.', | |
| `- Start with a heading: ## Release Notes — ${currentTag}`, | |
| '- Output clean Markdown, nothing else.', | |
| ); | |
| if (additionalRules) { | |
| lines.push('', 'Additional rules:', additionalRules); | |
| } | |
| systemPrompt = lines.join('\n'); | |
| } |
That removes the whitespace and token wasteage
- Fix script injection: pass all inputs via env vars instead of
inline template literals in github-script steps
- Add guard for unexpected API responses (missing choices array)
- Add tag-pattern input for repos using non-v* tag formats
- Add release-name input with {tag} placeholder for custom naming
- Refactor prompt construction to use array join instead of template
literals with YAML whitespace (saves tokens)
Co-authored-by: Albert Schermann <Bird87ZA@users.noreply.github.com>
Made-with: Cursor
|
All review feedback addressed in fd22ef3:
Ready for re-review. 🙌 |
Albert Scherman (Bird87ZA)
left a comment
There was a problem hiding this comment.
LGTM.
I've added Marcel Kräml (@mkraeml) just for an extra set of eyes.
Summary
ai-release-notescomposite action that generates AI-powered release notes and creates draft GitHub releasesnexus-workflow-builderinto a reusable, configurable actionContext
Ref: shopware/pipe-fiction#477
The AI release notes logic currently lives only in
nexus-workflow-builderas a 139-line inline workflow. Other repos (databus, nexus-contracts, nexus-service) cannot reuse it without copy-pasting.This action makes it available to any repo via:
What's included
ai-release-notes/action.ymlai-release-notes/README.mdREADME.mdDefault behaviour
With only
product-nameset, the action reproduces the current nexus-workflow-builder behaviour:Test plan
v*tag to a test repo using the action and verify the draft release is created with expected formattingcreate-release: "false"and verify notes are available via outputsinclude-pr-links: "true", different sections) and verify prompt changesnexus-workflow-builderto use this action and compare output with previous inline workflowMade with Cursor