Skip to content

feat: add ai-release-notes composite action#132

Open
simon (susishopware) wants to merge 2 commits intomainfrom
feat/ai-release-notes
Open

feat: add ai-release-notes composite action#132
simon (susishopware) wants to merge 2 commits intomainfrom
feat/ai-release-notes

Conversation

@susishopware
Copy link

Summary

  • Adds a new ai-release-notes composite action that generates AI-powered release notes and creates draft GitHub releases
  • Extracts and generalises the inline workflow logic from nexus-workflow-builder into a reusable, configurable action
  • All formatting options (bold features, PR links, authors, sections, collapse types) are configurable via inputs with sensible defaults

Context

Ref: shopware/pipe-fiction#477

The AI release notes logic currently lives only in nexus-workflow-builder as 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:

- uses: shopware/github-actions/ai-release-notes@main
  with:
    product-name: "My Product"

What's included

File Purpose
ai-release-notes/action.yml Composite action with 4 steps: detect previous tag, generate raw notes, AI rewrite, create release
ai-release-notes/README.md Documentation with inputs, outputs, and usage examples (minimal, customised, notes-only)
README.md Updated root README with new action in the Build & Release table

Default behaviour

With only product-name set, the action reproduces the current nexus-workflow-builder behaviour:

  • Sections: New Features, Improvements, Bug Fixes, Other
  • Bold feature names
  • No PR links, no author attributions
  • Dependency/translation/CI changes collapsed under "Other"
  • Draft release created

Test plan

  • Push a v* tag to a test repo using the action and verify the draft release is created with expected formatting
  • Test with create-release: "false" and verify notes are available via outputs
  • Test with custom inputs (include-pr-links: "true", different sections) and verify prompt changes
  • Wire nexus-workflow-builder to use this action and compare output with previous inline workflow

Made with Cursor

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
@susishopware
Copy link
Author

Feature Overview

Core Functionality (extracted from nexus-workflow-builder/.github/workflows/ai-release-notes.yml)

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

@susishopware
Copy link
Author

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.

Comment on lines +144 to +151
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 }}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';

}

const result = await response.json();
const aiNotes = result.choices[0].message.content;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a good idea to add a guard here just so we don't break when we find an unexpected response:

Suggested change
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;

owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
name: `Release ${tag}`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release is hardcoded. Maybe make this configurable too?

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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)

Comment on lines +154 to +187
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}`;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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
@susishopware
Copy link
Author

All review feedback addressed in fd22ef3:

# Feedback Fix
1 Script injection risk All inputs now passed via env: and read through process.env.* — no more inline ${{ inputs.* }} in script blocks
2 Missing API response guard Added result.choices?.length check before accessing the response
3 Hardcoded release name New release-name input with {tag} placeholder (default: Release {tag})
4 Hardcoded tag pattern New tag-pattern input (default: ^v) — supports ^release-, ^\d etc.
5 YAML whitespace in prompt Refactored to lines.push() + join('\n') — no more wasted tokens from indentation

Ready for re-review. 🙌

@susishopware simon (susishopware) marked this pull request as ready for review March 12, 2026 13:13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I've added Marcel Kräml (@mkraeml) just for an extra set of eyes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants