From 092d836f1c7f8f8ced17dca6ca9b2a864a33ac31 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Sat, 11 Jul 2026 01:24:59 +0200 Subject: [PATCH 1/3] Switch release changelog to Cloudflare Workers AI. Replace the Claude-based ccc action with a CI script using Kimi K2.7 on Workers AI so releases no longer depend on Anthropic credentials. --- .github/workflows/build.yml | 53 +++--------- package.json | 3 +- scripts/generate-ai-changelog.mjs | 137 ++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 42 deletions(-) create mode 100755 scripts/generate-ai-changelog.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ccc1bdb..f567cde2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,49 +34,13 @@ jobs: - name: Build id: build_code run: bun run build - - name: Detect previous tag - id: previous_tag - shell: bash - run: | - current_tag="${GITHUB_REF##*/}" - echo "current=$current_tag" >> "$GITHUB_OUTPUT" - if previous_tag=$(git describe --tags --abbrev=0 "${current_tag}^" 2>/dev/null); then - echo "found=true" >> "$GITHUB_OUTPUT" - echo "value=$previous_tag" >> "$GITHUB_OUTPUT" - else - echo "found=false" >> "$GITHUB_OUTPUT" - fi - name: Generate AI changelog id: changelog - if: ${{ steps.previous_tag.outputs.found == 'true' }} - uses: mistricky/ccc@v0.2.6 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - github_token: ${{ secrets.GITHUB_TOKEN }} - model: claude-sonnet-4-5-20250929 - - name: Prepare release notes - id: release_notes - shell: bash + continue-on-error: true env: - CURRENT_TAG: ${{ steps.previous_tag.outputs.current }} - PREVIOUS_TAG: ${{ steps.previous_tag.outputs.value }} - CHANGELOG_TEXT: ${{ steps.changelog.outputs.result }} - run: | - if [[ "${{ steps.previous_tag.outputs.found }}" == "true" ]]; then - { - printf '## 🆕 Changelog\n\n' - printf '%s\n\n' "$CHANGELOG_TEXT" - printf '%s\n\n' '---' - printf '🔗 **Full Changelog**: https://github.com/${{ github.repository }}/compare/%s...%s\n' "$PREVIOUS_TAG" "$CURRENT_TAG" - } > release-notes.md - else - { - printf '## 🆕 Changelog\n\n' - printf 'Initial release.\n\n' - printf '%s\n\n' '---' - printf '🔖 **Tag**: %s\n' "$CURRENT_TAG" - } > release-notes.md - fi + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: bun run changelog:ai - name: Setup Node.js for npm publish uses: actions/setup-node@v6 with: @@ -96,7 +60,14 @@ jobs: id: create_release uses: softprops/action-gh-release@v2 with: - body_path: release-notes.md + body: | + ## 🆕 Changelog + + ${{ steps.changelog.outputs.result || '_Changelog was not generated automatically for this release._' }} + + --- + + 🔗 **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.changelog.outputs.from_tag || github.ref_name }}...${{ steps.changelog.outputs.to_tag || github.ref_name }} make_latest: true token: '${{ secrets.PERSONAL_ACCESS_TOKEN }}' prerelease: ${{ contains(github.ref, '-alpha.') }} diff --git a/package.json b/package.json index 18797ad6..f513edbf 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "prepublishOnly": "bun run build", "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs", "example:install": "cd example-app && bun install --frozen-lockfile", - "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build" + "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build", + "changelog:ai": "node scripts/generate-ai-changelog.mjs" }, "dependencies": { "localforage": "^1.10.0" diff --git a/scripts/generate-ai-changelog.mjs b/scripts/generate-ai-changelog.mjs new file mode 100755 index 00000000..5b5d4c94 --- /dev/null +++ b/scripts/generate-ai-changelog.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +/** + * Generate a release changelog with Cloudflare Workers AI in CI. + * Invoked from build.yml on tag releases via `bun run changelog:ai`. + */ + +import { execFileSync } from 'node:child_process'; +import { appendFileSync } from 'node:fs'; + +const DEFAULT_MODEL = '@cf/moonshotai/kimi-k2.7-code'; + +function parseArgs(argv) { + const args = { fromTag: process.env.FROM_TAG, toTag: process.env.TO_TAG }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--from-tag') args.fromTag = argv[++i]; + if (argv[i] === '--to-tag') args.toTag = argv[++i]; + if (argv[i] === '--model') args.model = argv[++i]; + } + return args; +} + +function git(args) { + return execFileSync('git', args, { encoding: 'utf8' }).trim(); +} + +function resolveTags({ fromTag, toTag }) { + const currentTag = + toTag ?? + (process.env.GITHUB_REF?.startsWith('refs/tags/') + ? process.env.GITHUB_REF.replace('refs/tags/', '') + : null); + + if (!currentTag) { + throw new Error('Missing target tag. Set GITHUB_REF to refs/tags/ or pass --to-tag.'); + } + + const previousTag = fromTag ?? git(['describe', '--tags', '--abbrev=0', `${currentTag}^`]); + return { previousTag, currentTag }; +} + +function buildPrompt(previousTag, currentTag) { + const commits = git(['log', `${previousTag}..${currentTag}`, '--pretty=format:%s']); + const diffLines = git(['diff', '--stat', `${previousTag}..${currentTag}`]).split('\n'); + const diffStat = diffLines.at(-1)?.trim() ?? ''; + + return `You are a technical writer creating a changelog for a software project. + +There are changes in the git repository between two points: +- From tag: ${previousTag} +- To tag: ${currentTag} +- Commits: +${commits || '(none)'} +- Diff summary: ${diffStat || '(no file changes)'} + +## Instructions: +1. **Categorize changes** using standard changelog categories: + - **Added**: New features + - **Changed**: Changes in existing functionality + - **Deprecated**: Soon-to-be removed features + - **Removed**: Removed features + - **Fixed**: Bug fixes + - **Security**: Security improvements + +2. **Write clear, user-focused descriptions** that explain: + - What changed from a user's perspective + - Why it matters + - Any breaking changes or migration notes + +3. **Use consistent formatting**: + - Each item should be a concise bullet point + - Start with an action verb when possible + - Group related changes together + +4. **Focus on semantic meaning** rather than technical implementation details + +Output ONLY the changelog content in markdown format. Do NOT include any explanatory text, metadata, introductory phrases like "Based on the git analysis" or "Here's the changelog", or concluding remarks. Start IMMEDIATELY with the changelog categories and items (e.g., "## Added"). Do not include version numbers or dates. Do not preface your response with any text whatsoever.`; +} + +async function generateChangelog(prompt, model) { + const accountId = process.env.CLOUDFLARE_ACCOUNT_ID; + const apiToken = process.env.CLOUDFLARE_API_TOKEN; + + if (!accountId || !apiToken) { + throw new Error('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required.'); + } + + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`; + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: [{ role: 'user', content: prompt }], + }), + }); + + const payload = await response.json(); + if (!response.ok || !payload.success) { + throw new Error(`Cloudflare AI request failed with status ${response.status}`); + } + + const content = payload.result?.choices?.[0]?.message?.content ?? payload.result?.response; + if (!content || typeof content !== 'string') { + throw new Error('Cloudflare AI returned an empty changelog response'); + } + + return content.trim(); +} + +function writeGithubOutput({ result, fromTag, toTag }) { + const outputFile = process.env.GITHUB_OUTPUT; + if (!outputFile) return; + + const delimiter = `changelog_${Date.now()}`; + appendFileSync(outputFile, `result<<${delimiter}\n${result}\n${delimiter}\n`); + appendFileSync(outputFile, `from_tag=${fromTag}\n`); + appendFileSync(outputFile, `to_tag=${toTag}\n`); +} + +const args = parseArgs(process.argv.slice(2)); +const model = args.model ?? process.env.CLOUDFLARE_AI_MODEL ?? DEFAULT_MODEL; +const { previousTag, currentTag } = resolveTags(args); +const prompt = buildPrompt(previousTag, currentTag); + +console.error(`Generating changelog with ${model}`); +console.error(`Range: ${previousTag}..${currentTag}`); + +try { + const result = await generateChangelog(prompt, model); + writeGithubOutput({ result, fromTag: previousTag, toTag: currentTag }); +} catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(`Changelog generation failed: ${message}`); + process.exit(1); +} From 7435bf92bed86d1286ced36cef5d19728e526aef Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Sat, 11 Jul 2026 01:31:46 +0200 Subject: [PATCH 2/3] fix(ci): use absolute git path for changelog script. Satisfy Sonar PATH security hotspot when spawning git from Workers AI changelog generation. --- scripts/generate-ai-changelog.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate-ai-changelog.mjs b/scripts/generate-ai-changelog.mjs index 5b5d4c94..02ef99cd 100755 --- a/scripts/generate-ai-changelog.mjs +++ b/scripts/generate-ai-changelog.mjs @@ -20,7 +20,7 @@ function parseArgs(argv) { } function git(args) { - return execFileSync('git', args, { encoding: 'utf8' }).trim(); + return execFileSync('/usr/bin/git', args, { encoding: 'utf8' }).trim(); } function resolveTags({ fromTag, toTag }) { From 0aeff5600c756ec166a6f8673480978d897db222 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Sat, 11 Jul 2026 01:47:20 +0200 Subject: [PATCH 3/3] fix(ci): format changelog script for prettier/eslint. --- scripts/generate-ai-changelog.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/generate-ai-changelog.mjs b/scripts/generate-ai-changelog.mjs index 02ef99cd..fad185e9 100755 --- a/scripts/generate-ai-changelog.mjs +++ b/scripts/generate-ai-changelog.mjs @@ -26,9 +26,7 @@ function git(args) { function resolveTags({ fromTag, toTag }) { const currentTag = toTag ?? - (process.env.GITHUB_REF?.startsWith('refs/tags/') - ? process.env.GITHUB_REF.replace('refs/tags/', '') - : null); + (process.env.GITHUB_REF?.startsWith('refs/tags/') ? process.env.GITHUB_REF.replace('refs/tags/', '') : null); if (!currentTag) { throw new Error('Missing target tag. Set GITHUB_REF to refs/tags/ or pass --to-tag.');