|
| 1 | +#!/usr/bin/env bun |
| 2 | + |
| 3 | +import { $ } from "bun" |
| 4 | + |
| 5 | +const TEAM = ["actions-user", "github-actions[bot]", "code-yeongyu"] |
| 6 | + |
| 7 | +async function getLatestReleasedTag(): Promise<string | null> { |
| 8 | + try { |
| 9 | + const tag = await $`gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty'`.text() |
| 10 | + return tag.trim() || null |
| 11 | + } catch { |
| 12 | + return null |
| 13 | + } |
| 14 | +} |
| 15 | + |
| 16 | +async function generateChangelog(previousTag: string): Promise<string[]> { |
| 17 | + const notes: string[] = [] |
| 18 | + |
| 19 | + try { |
| 20 | + const log = await $`git log ${previousTag}..HEAD --oneline --format="%h %s"`.text() |
| 21 | + const commits = log |
| 22 | + .split("\n") |
| 23 | + .filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:|release:)/i)) |
| 24 | + |
| 25 | + if (commits.length > 0) { |
| 26 | + for (const commit of commits) { |
| 27 | + notes.push(`- ${commit}`) |
| 28 | + } |
| 29 | + } |
| 30 | + } catch { |
| 31 | + // No previous tags found |
| 32 | + } |
| 33 | + |
| 34 | + return notes |
| 35 | +} |
| 36 | + |
| 37 | +async function getContributors(previousTag: string): Promise<string[]> { |
| 38 | + const notes: string[] = [] |
| 39 | + |
| 40 | + try { |
| 41 | + const compare = |
| 42 | + await $`gh api "/repos/code-yeongyu/oh-my-opencode/compare/${previousTag}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text() |
| 43 | + const contributors = new Map<string, string[]>() |
| 44 | + |
| 45 | + for (const line of compare.split("\n").filter(Boolean)) { |
| 46 | + const { login, message } = JSON.parse(line) as { login: string | null; message: string } |
| 47 | + const title = message.split("\n")[0] ?? "" |
| 48 | + if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue |
| 49 | + |
| 50 | + if (login && !TEAM.includes(login)) { |
| 51 | + if (!contributors.has(login)) contributors.set(login, []) |
| 52 | + contributors.get(login)?.push(title) |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + if (contributors.size > 0) { |
| 57 | + notes.push("") |
| 58 | + notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`) |
| 59 | + for (const [username, userCommits] of contributors) { |
| 60 | + notes.push(`- @${username}:`) |
| 61 | + for (const commit of userCommits) { |
| 62 | + notes.push(` - ${commit}`) |
| 63 | + } |
| 64 | + } |
| 65 | + } |
| 66 | + } catch { |
| 67 | + // Failed to fetch contributors |
| 68 | + } |
| 69 | + |
| 70 | + return notes |
| 71 | +} |
| 72 | + |
| 73 | +async function main() { |
| 74 | + const previousTag = await getLatestReleasedTag() |
| 75 | + |
| 76 | + if (!previousTag) { |
| 77 | + console.log("Initial release") |
| 78 | + process.exit(0) |
| 79 | + } |
| 80 | + |
| 81 | + const changelog = await generateChangelog(previousTag) |
| 82 | + const contributors = await getContributors(previousTag) |
| 83 | + const notes = [...changelog, ...contributors] |
| 84 | + |
| 85 | + if (notes.length === 0) { |
| 86 | + console.log("No notable changes") |
| 87 | + } else { |
| 88 | + console.log(notes.join("\n")) |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +main() |
0 commit comments