Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
changelog:
categories:
- title: '🚀 New Features'
labels:
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'bug'
- title: '📦 Dependency Updates'
labels:
- 'dependencies'
- title: '🧰 Maintenance & Documentation'
labels:
- 'maintenance'
- 'documentation'
233 changes: 233 additions & 0 deletions .github/scripts/generate-fdroid-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'

interface GitHubLabel {
name: string
}

interface PullRequest {
number: number
title: string
labels: GitHubLabel[]
}

// Helper to run shell commands
function runCmd(cmd: string): string | null {
try {
return execSync(cmd, { encoding: 'utf8' }).trim()
} catch (error: any) {
console.error(`Command failed: ${cmd}`, error.message)
return null
}
}

// Helper to extract repo owner and name from remote URL (useful for local testing)
function getRepoFromGit(): string | null {
const remotes = runCmd('git remote -v')
if (!remotes) return null
// Matches git@github.com:owner/repo.git or https://github.com/owner/repo.git
const match = remotes.match(/github\.com[:/]([^/]+)\/([^.\s]+)/)
if (match) {
return `${match[1]}/${match[2]}`
}
return null
}

async function main(): Promise<void> {
const newVersionCode = process.argv[2]
if (!newVersionCode) {
console.error('Error: Please provide version code as an argument.')
process.exit(1)
}

const currentTag = process.env.GITHUB_REF_NAME || process.argv[3]
if (!currentTag) {
console.error(
'Error: GITHUB_REF_NAME environment variable is not set and no tag was passed as third argument.',
)
process.exit(1)
}

const githubToken = process.env.GITHUB_TOKEN
const githubRepository = process.env.GITHUB_REPOSITORY || getRepoFromGit()
if (!githubRepository) {
console.error(
'Error: GITHUB_REPOSITORY environment variable is not set and could not be inferred.',
)
process.exit(1)
}

const [owner, repo] = githubRepository.split('/')

console.log(`Current tag: ${currentTag}`)
console.log(`Version code: ${newVersionCode}`)
console.log(`Repository: ${owner}/${repo}`)

// Find the previous tag
let prevTag = runCmd(`git describe --tags --abbrev=0 ${currentTag}~1`)
if (!prevTag) {
console.log(
'No previous tag found using git describe. Finding via tag list...',
)
const tagsList =
runCmd(`git tag --sort=-v:refname`)?.split('\n')?.filter(Boolean) ||
[]
const currentIndex = tagsList.indexOf(currentTag)
if (currentIndex !== -1 && currentIndex + 1 < tagsList.length) {
prevTag = tagsList[currentIndex + 1]
}
}

console.log(`Previous tag: ${prevTag || 'None'}`)

// Get list of commit messages
const range = prevTag ? `${prevTag}..${currentTag}` : currentTag
const commits = runCmd(`git log ${range} --oneline`)
if (!commits) {
console.log('No commits found in the range.')
writeChangelog(newVersionCode, [])
return
}

// Extract PR numbers from commit history
const prNumbers = new Set<number>()
const lines = commits.split('\n')
for (const line of lines) {
const mergeMatch = line.match(/Merge pull request #(\d+)/i)
if (mergeMatch) {
prNumbers.add(parseInt(mergeMatch[1], 10))
continue
}
const squashMatch = line.match(/\(#(\d+)\)$/)
if (squashMatch) {
prNumbers.add(parseInt(squashMatch[1], 10))
}
}

console.log(`Identified PRs: ${Array.from(prNumbers).join(', ') || 'None'}`)

if (prNumbers.size === 0) {
console.log('No PRs found in the commit range.')
writeChangelog(newVersionCode, [])
return
}

// Fetch details for each PR from the GitHub API
const prDetails: PullRequest[] = []
const headers: Record<string, string> = {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'github-action-changelog-generator',
}
if (githubToken) {
headers.Authorization = `Bearer ${githubToken}`
} else {
console.warn(
'Warning: GITHUB_TOKEN not set. Making unauthenticated API requests.',
)
}

for (const prNumber of prNumbers) {
try {
console.log(`Fetching details for PR #${prNumber}...`)
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
{ headers },
)

if (!response.ok) {
console.error(
`Failed to fetch PR #${prNumber}: ${response.statusText}`,
)
continue
}

const data = (await response.json()) as PullRequest
prDetails.push(data)
} catch (error: any) {
console.error(`Error fetching PR #${prNumber}:`, error.message)
}
}

// Filter PRs to only those labeled 'bug' or 'enhancement'
const filteredPrs = prDetails.filter((pr) => {
const labels = pr.labels || []
return labels.some((label) => {
const name = label.name.toLowerCase()
return name === 'bug' || name === 'enhancement'
})
})

// Sort PRs so enhancements are listed first, then bugs
filteredPrs.sort((a, b) => {
const aIsEnhancement = a.labels.some(
(l) => l.name.toLowerCase() === 'enhancement',
)
const bIsEnhancement = b.labels.some(
(l) => l.name.toLowerCase() === 'enhancement',
)
if (aIsEnhancement && !bIsEnhancement) return -1
if (!aIsEnhancement && bIsEnhancement) return 1
return 0
})

// Format the changelog entries
const changelogLines = filteredPrs.map((pr) => {
const isBug = pr.labels.some((l) => l.name.toLowerCase() === 'bug')
const prefix = isBug ? 'Fix: ' : 'New: '
return `- ${prefix}${pr.title} (#${pr.number})`
})

writeChangelog(newVersionCode, changelogLines)
}

function writeChangelog(
baseVersionCodeStr: string,
changelogLines: string[],
): void {
const baseVersionCode = parseInt(baseVersionCodeStr, 10)
if (isNaN(baseVersionCode)) {
console.error(
`Error: baseVersionCode "${baseVersionCodeStr}" is not a number.`,
)
process.exit(1)
}

// F-Droid builds use versionCode suffixes: base * 10 + 1 and base * 10 + 2
const versionCodes = [baseVersionCode * 10 + 1, baseVersionCode * 10 + 2]

const generalNote =
'- General improvements to app stability, dependencies, and code maintenance.'
let changelogText = ''

if (changelogLines.length > 0) {
changelogText = changelogLines.join('\n') + '\n' + generalNote
} else {
changelogText = generalNote
}

const languages = ['de', 'en-US']
for (const lang of languages) {
const dirPath = path.join(
__dirname,
'..',
'..',
'metadata',
lang,
'changelogs',
)
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
for (const vc of versionCodes) {
const filePath = path.join(dirPath, `${vc}.txt`)
fs.writeFileSync(filePath, changelogText + '\n', 'utf8')
console.log(`Successfully wrote changelog to: ${filePath}`)
}
}
}

main().catch((err) => {
console.error('Unhandled error in script:', err)
process.exit(1)
})
13 changes: 12 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
new_version_code: ${{ steps.bump.outputs.new_version_code }}
Expand All @@ -36,12 +37,22 @@ jobs:
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "new_version_code=$NEW_VERSION_CODE" >> $GITHUB_OUTPUT

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24

- name: Generate F-Droid Changelog
env:
GITHUB_TOKEN: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
run: npx tsx .github/scripts/generate-fdroid-changelog.ts ${{ steps.bump.outputs.new_version_code }}

- name: Commit and push changes
id: auto_commit
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
with:
commit_message: 'chore: bump version to ${{ steps.bump.outputs.new_version }} and versionCode to ${{ steps.bump.outputs.new_version_code }} [skip ci]'
file_pattern: 'package.json'
file_pattern: 'package.json metadata/'

- name: Move tag to new commit
if: steps.auto_commit.outputs.changes_detected == 'true'
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig(
'coverage/**',
'.yarn/**',
'node_modules/**',
'.github/scripts/**',
],
},
eslint.configs.recommended,
Expand Down