lechnerio-git-hooks 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,7 @@ On install, the package automatically:
36
36
 
37
37
  ## AI release summaries
38
38
 
39
- On Minor and Major version bumps, the `post-commit` hook sends the version's non-skipped commit messages to the Gemini API and stores the result as a `summary` field on the version entry in `data/changelog.json`. This requires `GITHOOKS_GEMINI_API_KEY` in the project's `.env` file or environment; without it the summary step is skipped silently.
39
+ On Minor and Major version bumps, the `post-commit` hook sends the version's non-skipped commit messages to the Gemini API and stores the result as `summary_en` and `summary_ger` fields (English and German) on the version entry in `data/changelog.json`. This requires `GITHOOKS_GEMINI_API_KEY` in the project's `.env` file or environment; without it the summary step is skipped silently.
40
40
 
41
41
  ## Peer dependencies (auto-installed)
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lechnerio-git-hooks",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Shared git hooks for lechnerio projects",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -36,7 +36,14 @@ if (existsSync("data/changelog.json")) {
36
36
  try {
37
37
  const existing = JSON.parse(readFileSync("data/changelog.json", "utf-8"))
38
38
  for (const entry of existing) {
39
- if (typeof entry.summary === "string") summaries.set(entry.version, entry.summary)
39
+ const summaryEn = entry.summary_en ?? entry.summary
40
+ const summaryGer = entry.summary_ger
41
+ if (typeof summaryEn === "string" || typeof summaryGer === "string") {
42
+ const saved = {}
43
+ if (typeof summaryEn === "string") saved.summary_en = summaryEn
44
+ if (typeof summaryGer === "string") saved.summary_ger = summaryGer
45
+ summaries.set(entry.version, saved)
46
+ }
40
47
  for (const commit of entry.commits ?? []) {
41
48
  skipFlags.set(commit.message, commit.skip === true)
42
49
  }
@@ -86,10 +93,11 @@ const baseVersion = (v) => v.replace(/[a-z]+$/i, "")
86
93
  const grouped = []
87
94
  for (const entry of changelog) {
88
95
  const base = baseVersion(entry.version)
89
- const existing = grouped.find((g) => g.version === base && g.tag !== null)
96
+ const existing = grouped.find((g) => g.version === base)
90
97
  if (existing) {
91
98
  existing.commits.push(...entry.commits)
92
99
  if (entry.date < existing.date) existing.date = entry.date
100
+ if (entry.tag && (!existing.tag || entry.tag.replace(/^v/, "") === base)) existing.tag = entry.tag
93
101
  } else {
94
102
  grouped.push({ ...entry, version: base })
95
103
  }
@@ -106,7 +114,10 @@ for (const entry of grouped) {
106
114
 
107
115
  for (const entry of grouped) {
108
116
  const saved = summaries.get(entry.version)
109
- if (saved && !entry.summary) entry.summary = saved
117
+ if (saved) {
118
+ if (saved.summary_en && !entry.summary_en) entry.summary_en = saved.summary_en
119
+ if (saved.summary_ger && !entry.summary_ger) entry.summary_ger = saved.summary_ger
120
+ }
110
121
  }
111
122
 
112
123
  if (process.argv.includes("--summarize")) {
@@ -114,33 +125,40 @@ if (process.argv.includes("--summarize")) {
114
125
  if (!apiKey) {
115
126
  console.warn("GITHOOKS_GEMINI_API_KEY not set, skipping AI summary")
116
127
  } else {
117
- const target = grouped.find((e) => e.tag === tags[0])
118
- if (target && !target.summary) {
128
+ const target = grouped.find((e) => e.version === baseVersion(tags[0].replace(/^v/, "")))
129
+ if (target && !(target.summary_en && target.summary_ger)) {
119
130
  const messages = target.commits.filter((c) => !c.skip).map((c) => c.message)
120
131
  if (messages.length === 0) {
121
132
  console.warn("no commits to summarize")
122
133
  } else {
123
- try {
124
- const model = loadEnvKey("GITHOOKS_GEMINI_MODEL") ?? "gemini-flash-latest"
125
- const prompt = `Summarize these commit messages into a short user-facing release summary for version ${target.version}. Write 1-3 plain sentences, no markdown, no bullet points, no preamble.\n\n${messages.join("\n")}`
126
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
127
- method: "POST",
128
- headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
129
- body: JSON.stringify({
130
- contents: [{ parts: [{ text: prompt }] }],
131
- generationConfig: { thinkingConfig: { thinkingBudget: 0 } },
132
- }),
133
- signal: AbortSignal.timeout(60000),
134
- })
135
- if (!res.ok) throw new Error(`gemini api returned ${res.status}`)
136
- const data = await res.json()
137
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim()
138
- if (text) {
139
- target.summary = text
134
+ const model = loadEnvKey("GITHOOKS_GEMINI_MODEL") ?? "gemini-flash-latest"
135
+ const prompt = `Summarize these commit messages into a short user-facing release summary for version ${target.version}. Write 1-3 plain sentences per language, no markdown, no bullet points, no preamble. Return a JSON object with exactly two string keys: "summary_en" (English) and "summary_ger" (German).\n\n${messages.join("\n")}`
136
+ for (let attempt = 1; attempt <= 2; attempt++) {
137
+ try {
138
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
141
+ body: JSON.stringify({
142
+ contents: [{ parts: [{ text: prompt }] }],
143
+ generationConfig: { responseMimeType: "application/json", thinkingConfig: { thinkingBudget: 0 } },
144
+ }),
145
+ signal: AbortSignal.timeout(60000),
146
+ })
147
+ if (!res.ok) throw new Error(`gemini api returned ${res.status}`)
148
+ const data = await res.json()
149
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim()
150
+ if (!text) throw new Error("empty summary response")
151
+ const parsed = JSON.parse(text)
152
+ if (typeof parsed.summary_en !== "string" || typeof parsed.summary_ger !== "string") {
153
+ throw new Error("unexpected summary response shape")
154
+ }
155
+ target.summary_en = parsed.summary_en.trim()
156
+ target.summary_ger = parsed.summary_ger.trim()
140
157
  console.log(`ai summary generated for ${target.version}`)
158
+ break
159
+ } catch (err) {
160
+ if (attempt === 2) console.warn(`ai summary failed: ${err.message}`)
141
161
  }
142
- } catch (err) {
143
- console.warn(`ai summary failed: ${err.message}`)
144
162
  }
145
163
  }
146
164
  }