From cd9f4293f73e1b781eceb1102e5b0a8e8b615e78 Mon Sep 17 00:00:00 2001 From: susishopware Date: Tue, 4 Aug 2026 14:29:35 +0200 Subject: [PATCH] fix(ai-release-notes): migrate off retired GitHub Models API GitHub Models was fully retired on July 30, 2026, which broke every release workflow using this action (401 from the legacy inference endpoint). Replace the hardcoded GitHub Models call with a configurable OpenAI-compatible endpoint and a new api-key input, and fall back to the raw GitHub changelog instead of failing the release when no key is configured or the AI call errors. Co-Authored-By: Claude Fable 5 --- ai-release-notes/README.md | 30 ++++++++++++++++++++++++------ ai-release-notes/action.yml | 34 ++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/ai-release-notes/README.md b/ai-release-notes/README.md index 3232174..4724851 100644 --- a/ai-release-notes/README.md +++ b/ai-release-notes/README.md @@ -2,14 +2,17 @@ Generate polished, AI-powered release notes from GitHub's auto-generated changelog and optionally create a draft GitHub release. -The action compares the current tag with the previous one, fetches the raw changelog via the GitHub API, rewrites it using the [GitHub Models API](https://docs.github.com/en/github-models), and creates a draft release with the result. +The action compares the current tag with the previous one, fetches the raw changelog via the GitHub API, rewrites it using an OpenAI-compatible chat completions API, and creates a draft release with the result. + +> **Note:** This action previously used the GitHub Models API, which was [fully retired on July 30, 2026](https://github.blog/changelog/2026-07-30-github-models-is-now-retired/). It now calls a configurable OpenAI-compatible endpoint instead and requires an `api-key`. Without an `api-key`, the AI rewrite is skipped and the raw GitHub changelog is used — the release is still created. ## Prerequisites The calling workflow must: 1. **Check out the repository** with full history (`fetch-depth: 0`) so previous tags can be detected. -2. **Grant permissions** for `contents: write` (to create releases) and `models: read` (to call the AI API). +2. **Grant permissions** for `contents: write` (to create releases). +3. **Provide an API key** for an OpenAI-compatible provider via the `api-key` input (typically an org/repo secret). Without it, the release falls back to the raw GitHub changelog. ## Usage @@ -25,7 +28,6 @@ on: permissions: contents: write - models: read jobs: release-notes: @@ -38,6 +40,20 @@ jobs: - uses: shopware/github-actions/ai-release-notes@main with: product-name: "My Product" + api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### Using a Different Provider + +Any OpenAI-compatible chat completions endpoint works — for example the Anthropic API: + +```yaml + - uses: shopware/github-actions/ai-release-notes@main + with: + product-name: "My Product" + api-endpoint: "https://api.anthropic.com/v1/chat/completions" + api-key: ${{ secrets.ANTHROPIC_API_KEY }} + model: "claude-sonnet-5" ``` ### Customised Example @@ -87,20 +103,22 @@ Use the action as a pure notes generator — for example to post to Slack or app | `collapse-types` | no | `"dependency,translation,CI"` | Change types collapsed under the last section | | `additional-rules` | no | `""` | Extra rules appended to the default prompt | | `custom-prompt` | no | `""` | Completely override the system prompt (ignores all formatting inputs) | -| `model` | no | `"gpt-4o"` | GitHub Models API model name | +| `api-endpoint` | no | `"https://api.openai.com/v1/chat/completions"` | OpenAI-compatible chat completions endpoint URL | +| `api-key` | no | `""` | API key for the AI provider — when empty, the AI rewrite is skipped and the raw changelog is used | +| `model` | no | `"gpt-4o"` | Model name understood by the configured endpoint | | `temperature` | no | `"0.4"` | AI temperature (`0.0` = deterministic, `1.0` = creative) | | `tag-pattern` | no | `"^v"` | Regex pattern to match tags when detecting the previous release | | `release-name` | no | `"Release {tag}"` | Release name template — use `{tag}` as placeholder for the tag name | | `draft` | no | `"true"` | Create the release as a draft | | `prerelease` | no | `"false"` | Mark the release as a prerelease | | `create-release` | no | `"true"` | Whether to create a GitHub release (`"false"` = only generate notes) | -| `github-token` | no | `${{ github.token }}` | GitHub token (needs `models:read` + `contents:write`) | +| `github-token` | no | `${{ github.token }}` | GitHub token (needs `contents:write`) | ## Outputs | Output | Description | |---|---| -| `release-notes` | The AI-generated release notes (Markdown) | +| `release-notes` | The AI-generated release notes (Markdown) — or the raw changelog when the AI rewrite was skipped | | `raw-notes` | The raw GitHub-generated changelog before AI rewrite | | `release-url` | URL of the created release (empty if `create-release` is `"false"`) | | `previous-tag` | The detected previous git tag | diff --git a/ai-release-notes/action.yml b/ai-release-notes/action.yml index e92aa8b..f8acc50 100644 --- a/ai-release-notes/action.yml +++ b/ai-release-notes/action.yml @@ -41,8 +41,16 @@ inputs: description: "Completely override the default system prompt (ignores all formatting inputs)" required: false default: "" + api-endpoint: + description: "OpenAI-compatible chat completions endpoint URL" + required: false + default: "https://api.openai.com/v1/chat/completions" + api-key: + description: "API key for the AI provider. When empty, the AI rewrite is skipped and the raw GitHub changelog is used as release notes." + required: false + default: "" model: - description: "GitHub Models API model name" + description: "Model name understood by the configured endpoint" required: false default: "gpt-4o" temperature: @@ -70,13 +78,13 @@ inputs: required: false default: "true" github-token: - description: "GitHub token with models:read and contents:write permissions" + description: "GitHub token with contents:write permission" required: false default: "${{ github.token }}" outputs: release-notes: - description: "The AI-generated release notes (Markdown)" + description: "The AI-generated release notes (Markdown) — or the raw changelog when the AI rewrite was skipped" value: "${{ steps.ai-notes.outputs.notes }}" raw-notes: description: "The raw GitHub-generated changelog before AI rewrite" @@ -140,7 +148,8 @@ runs: id: ai-notes uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GITHUB_TOKEN: "${{ inputs.github-token }}" + INPUT_API_ENDPOINT: "${{ inputs.api-endpoint }}" + INPUT_API_KEY: "${{ inputs.api-key }}" RAW_NOTES: "${{ steps.raw-notes.outputs.body }}" INPUT_PRODUCT_NAME: "${{ inputs.product-name }}" INPUT_PRODUCT_DESCRIPTION: "${{ inputs.product-description }}" @@ -158,6 +167,13 @@ runs: const currentTag = process.env.GITHUB_REF_NAME; const rawNotes = process.env.RAW_NOTES; const customPrompt = process.env.INPUT_CUSTOM_PROMPT; + const apiKey = process.env.INPUT_API_KEY; + + if (!apiKey) { + core.warning('No api-key configured — skipping AI rewrite and using the raw GitHub changelog as release notes.'); + core.setOutput('notes', rawNotes); + return; + } let systemPrompt; @@ -216,11 +232,11 @@ runs: systemPrompt = lines.join('\n'); } - const response = await fetch('https://models.inference.ai.azure.com/chat/completions', { + const response = await fetch(process.env.INPUT_API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, + 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: process.env.INPUT_MODEL, @@ -234,14 +250,16 @@ runs: if (!response.ok) { const errorBody = await response.text(); - core.setFailed(`GitHub Models API error (${response.status}): ${errorBody}`); + core.warning(`AI API error (${response.status}): ${errorBody} — falling back to the raw GitHub changelog.`); + core.setOutput('notes', rawNotes); return; } const result = await response.json(); if (!result.choices?.length) { - core.setFailed(`Unexpected API response: ${JSON.stringify(result)}`); + core.warning(`Unexpected AI API response: ${JSON.stringify(result)} — falling back to the raw GitHub changelog.`); + core.setOutput('notes', rawNotes); return; }