@openhands/extensions 0.1.0 → 0.2.0

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.
Files changed (347) hide show
  1. package/.agents/skills/custom-codereview-guide.md +25 -0
  2. package/.github/pull_request_template.md +38 -0
  3. package/.github/release.yml +14 -0
  4. package/.github/workflows/check-extensions.yml +72 -0
  5. package/.github/workflows/npm-publish.yml +89 -0
  6. package/.github/workflows/pr.yml +30 -0
  7. package/.github/workflows/release.yml +24 -0
  8. package/.github/workflows/tests.yml +25 -0
  9. package/.github/workflows/vulnerability-scan.yml +87 -0
  10. package/.release-please-manifest.json +3 -0
  11. package/AGENTS.md +132 -0
  12. package/README.md +10 -0
  13. package/analysis_results.md +162 -0
  14. package/marketplaces/large-codebase.json +66 -0
  15. package/marketplaces/openhands-extensions.json +682 -0
  16. package/package.json +4 -10
  17. package/plugins/README.md +30 -0
  18. package/plugins/city-weather/.plugin/plugin.json +13 -0
  19. package/plugins/city-weather/README.md +145 -0
  20. package/plugins/city-weather/commands/now.md +56 -0
  21. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  22. package/plugins/cobol-modernization/README.md +201 -0
  23. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  24. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  25. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  26. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  27. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  28. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  29. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  30. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  31. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  33. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  34. package/plugins/issue-duplicate-checker/README.md +51 -0
  35. package/plugins/issue-duplicate-checker/action.yml +349 -0
  36. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  37. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  38. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  39. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  40. package/plugins/magic-test/.plugin/plugin.json +13 -0
  41. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  42. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  43. package/plugins/migration-scoring/README.md +244 -0
  44. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  45. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  46. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  47. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  48. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  49. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  50. package/plugins/onboarding/.plugin/plugin.json +20 -0
  51. package/plugins/onboarding/README.md +30 -0
  52. package/plugins/onboarding/references/criteria.md +144 -0
  53. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  54. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  60. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  61. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  62. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  63. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  64. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  65. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  66. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  67. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  68. package/plugins/openhands/.plugin/plugin.json +13 -0
  69. package/plugins/openhands/README.md +52 -0
  70. package/plugins/openhands/SKILL.md +61 -0
  71. package/plugins/openhands/commands/create.md +55 -0
  72. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  73. package/plugins/openhands/scripts/run.sh +69 -0
  74. package/plugins/pr-review/.plugin/plugin.json +13 -0
  75. package/plugins/pr-review/README.md +393 -0
  76. package/plugins/pr-review/action.yml +298 -0
  77. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  78. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  79. package/plugins/pr-review/scripts/prompt.py +260 -0
  80. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  81. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  82. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  83. package/plugins/qa-changes/README.md +185 -0
  84. package/plugins/qa-changes/action.yml +181 -0
  85. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  86. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  87. package/plugins/qa-changes/scripts/prompt.py +174 -0
  88. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  89. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  90. package/plugins/release-notes/.plugin/plugin.json +19 -0
  91. package/plugins/release-notes/README.md +283 -0
  92. package/plugins/release-notes/SKILL.md +83 -0
  93. package/plugins/release-notes/action.yml +117 -0
  94. package/plugins/release-notes/commands/release-notes.md +8 -0
  95. package/plugins/release-notes/scripts/agent_script.py +292 -0
  96. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  97. package/plugins/release-notes/scripts/prompt.py +90 -0
  98. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  99. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  100. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  101. package/plugins/vulnerability-remediation/README.md +217 -0
  102. package/plugins/vulnerability-remediation/action.yml +187 -0
  103. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  104. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  105. package/pyproject.toml +12 -0
  106. package/release-please-config.json +16 -0
  107. package/scripts/sync_extensions.py +494 -0
  108. package/scripts/sync_openhands_sdk_skill.py +264 -0
  109. package/skills/README.md +159 -0
  110. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/README.md +40 -0
  112. package/skills/add-javadoc/SKILL.md +35 -0
  113. package/skills/add-javadoc/references/example.md +32 -0
  114. package/skills/add-skill/.plugin/plugin.json +18 -0
  115. package/skills/add-skill/README.md +67 -0
  116. package/skills/add-skill/SKILL.md +47 -0
  117. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  118. package/skills/agent-creator/.plugin/plugin.json +20 -0
  119. package/skills/agent-creator/README.md +104 -0
  120. package/skills/agent-creator/SKILL.md +190 -0
  121. package/skills/agent-creator/commands/agent-creator.md +8 -0
  122. package/skills/agent-creator/references/fallback.md +117 -0
  123. package/skills/agent-memory/.plugin/plugin.json +18 -0
  124. package/skills/agent-memory/README.md +35 -0
  125. package/skills/agent-memory/SKILL.md +30 -0
  126. package/skills/agent-memory/commands/remember.md +8 -0
  127. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/README.md +40 -0
  129. package/skills/agent-sdk-builder/SKILL.md +37 -0
  130. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  131. package/skills/azure-devops/.plugin/plugin.json +18 -0
  132. package/skills/azure-devops/README.md +55 -0
  133. package/skills/azure-devops/SKILL.md +50 -0
  134. package/skills/bitbucket/.plugin/plugin.json +17 -0
  135. package/skills/bitbucket/README.md +50 -0
  136. package/skills/bitbucket/SKILL.md +45 -0
  137. package/skills/code-review/.plugin/plugin.json +19 -0
  138. package/skills/code-review/README.md +18 -0
  139. package/skills/code-review/SKILL.md +208 -0
  140. package/skills/code-review/commands/codereview-roasted.md +8 -0
  141. package/skills/code-review/commands/codereview.md +8 -0
  142. package/skills/code-review/references/risk-evaluation.md +41 -0
  143. package/skills/code-review/references/supply-chain-security.md +31 -0
  144. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  145. package/skills/code-simplifier/README.md +30 -0
  146. package/skills/code-simplifier/SKILL.md +91 -0
  147. package/skills/code-simplifier/commands/simplify.md +8 -0
  148. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  149. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  150. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  151. package/skills/datadog/.plugin/plugin.json +19 -0
  152. package/skills/datadog/README.md +100 -0
  153. package/skills/datadog/SKILL.md +95 -0
  154. package/skills/deno/.plugin/plugin.json +18 -0
  155. package/skills/deno/README.md +5 -0
  156. package/skills/deno/SKILL.md +99 -0
  157. package/skills/deno/references/README.md +6 -0
  158. package/skills/discord/.plugin/plugin.json +18 -0
  159. package/skills/discord/README.md +31 -0
  160. package/skills/discord/SKILL.md +109 -0
  161. package/skills/discord/__init__.py +0 -0
  162. package/skills/discord/references/REFERENCE.md +78 -0
  163. package/skills/discord/scripts/__init__.py +0 -0
  164. package/skills/discord/scripts/_http.py +127 -0
  165. package/skills/discord/scripts/post_webhook.py +106 -0
  166. package/skills/discord/scripts/send_message.py +102 -0
  167. package/skills/docker/.plugin/plugin.json +17 -0
  168. package/skills/docker/README.md +34 -0
  169. package/skills/docker/SKILL.md +29 -0
  170. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  171. package/skills/evidence-based-citations/README.md +31 -0
  172. package/skills/evidence-based-citations/SKILL.md +59 -0
  173. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  174. package/skills/flarglebargle/README.md +14 -0
  175. package/skills/flarglebargle/SKILL.md +9 -0
  176. package/skills/frontend-design/.plugin/plugin.json +21 -0
  177. package/skills/frontend-design/LICENSE.txt +177 -0
  178. package/skills/frontend-design/README.md +42 -0
  179. package/skills/frontend-design/SKILL.md +42 -0
  180. package/skills/github/.plugin/plugin.json +19 -0
  181. package/skills/github/README.md +42 -0
  182. package/skills/github/SKILL.md +106 -0
  183. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  184. package/skills/github-pr-review/README.md +145 -0
  185. package/skills/github-pr-review/SKILL.md +148 -0
  186. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  187. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  188. package/skills/github-pr-reviewer/README.md +34 -0
  189. package/skills/github-pr-reviewer/SKILL.md +89 -0
  190. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  191. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  192. package/skills/github-repo-monitor/README.md +70 -0
  193. package/skills/github-repo-monitor/SKILL.md +316 -0
  194. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  195. package/skills/github-repo-monitor/references/github-api.md +241 -0
  196. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  197. package/skills/github-repo-monitor/scripts/main.py +915 -0
  198. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  199. package/skills/gitlab/.plugin/plugin.json +17 -0
  200. package/skills/gitlab/README.md +37 -0
  201. package/skills/gitlab/SKILL.md +32 -0
  202. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  203. package/skills/incident-retrospective/README.md +34 -0
  204. package/skills/incident-retrospective/SKILL.md +98 -0
  205. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  206. package/skills/iterate/.plugin/plugin.json +13 -0
  207. package/skills/iterate/README.md +25 -0
  208. package/skills/iterate/SKILL.md +399 -0
  209. package/skills/iterate/commands/babysit.md +8 -0
  210. package/skills/iterate/commands/iterate.md +8 -0
  211. package/skills/iterate/commands/verify.md +8 -0
  212. package/skills/iterate/references/heuristics.md +58 -0
  213. package/skills/iterate/references/verification.md +96 -0
  214. package/skills/jupyter/.plugin/plugin.json +18 -0
  215. package/skills/jupyter/README.md +55 -0
  216. package/skills/jupyter/SKILL.md +50 -0
  217. package/skills/kubernetes/.plugin/plugin.json +18 -0
  218. package/skills/kubernetes/README.md +53 -0
  219. package/skills/kubernetes/SKILL.md +48 -0
  220. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  221. package/skills/learn-from-code-review/README.md +64 -0
  222. package/skills/learn-from-code-review/SKILL.md +186 -0
  223. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  224. package/skills/linear/.plugin/plugin.json +19 -0
  225. package/skills/linear/README.md +58 -0
  226. package/skills/linear/SKILL.md +213 -0
  227. package/skills/linear-triage/.plugin/plugin.json +21 -0
  228. package/skills/linear-triage/README.md +34 -0
  229. package/skills/linear-triage/SKILL.md +91 -0
  230. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  231. package/skills/notion/.plugin/plugin.json +17 -0
  232. package/skills/notion/README.md +114 -0
  233. package/skills/notion/SKILL.md +109 -0
  234. package/skills/npm/.plugin/plugin.json +17 -0
  235. package/skills/npm/README.md +14 -0
  236. package/skills/npm/SKILL.md +9 -0
  237. package/skills/openhands-api/.plugin/plugin.json +22 -0
  238. package/skills/openhands-api/README.md +48 -0
  239. package/skills/openhands-api/SKILL.md +399 -0
  240. package/skills/openhands-api/references/README.md +33 -0
  241. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  242. package/skills/openhands-api/references/example_prompt.md +12 -0
  243. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  244. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  245. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  246. package/skills/openhands-automation/README.md +89 -0
  247. package/skills/openhands-automation/SKILL.md +875 -0
  248. package/skills/openhands-automation/commands/automation:create.md +8 -0
  249. package/skills/openhands-automation/references/ab-testing.md +185 -0
  250. package/skills/openhands-automation/references/custom-automation.md +644 -0
  251. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  252. package/skills/openhands-sdk/README.md +22 -0
  253. package/skills/openhands-sdk/SKILL.md +229 -0
  254. package/skills/openhands-sdk/commands/sdk.md +8 -0
  255. package/skills/pdflatex/.plugin/plugin.json +18 -0
  256. package/skills/pdflatex/README.md +39 -0
  257. package/skills/pdflatex/SKILL.md +34 -0
  258. package/skills/prd/.plugin/plugin.json +19 -0
  259. package/skills/prd/README.md +28 -0
  260. package/skills/prd/SKILL.md +237 -0
  261. package/skills/prd/commands/prd.md +8 -0
  262. package/skills/qa-changes/README.md +18 -0
  263. package/skills/qa-changes/SKILL.md +229 -0
  264. package/skills/qa-changes/commands/qa-changes.md +8 -0
  265. package/skills/release-notes/README.md +24 -0
  266. package/skills/release-notes/SKILL.md +19 -0
  267. package/skills/release-notes/commands/release-notes.md +8 -0
  268. package/skills/research-brief/.plugin/plugin.json +20 -0
  269. package/skills/research-brief/README.md +34 -0
  270. package/skills/research-brief/SKILL.md +99 -0
  271. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  272. package/skills/security/.plugin/plugin.json +18 -0
  273. package/skills/security/README.md +38 -0
  274. package/skills/security/SKILL.md +33 -0
  275. package/skills/skill-creator/.plugin/plugin.json +17 -0
  276. package/skills/skill-creator/LICENSE.txt +202 -0
  277. package/skills/skill-creator/README.md +182 -0
  278. package/skills/skill-creator/SKILL.md +545 -0
  279. package/skills/skill-creator/references/output-patterns.md +82 -0
  280. package/skills/skill-creator/references/workflows.md +28 -0
  281. package/skills/skill-creator/scripts/init_skill.py +303 -0
  282. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  283. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  284. package/skills/slack-channel-monitor/README.md +91 -0
  285. package/skills/slack-channel-monitor/SKILL.md +276 -0
  286. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  287. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  288. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  289. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  290. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  291. package/skills/slack-standup-digest/README.md +34 -0
  292. package/skills/slack-standup-digest/SKILL.md +92 -0
  293. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  294. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  295. package/skills/spark-version-upgrade/README.md +54 -0
  296. package/skills/spark-version-upgrade/SKILL.md +233 -0
  297. package/skills/ssh/.plugin/plugin.json +18 -0
  298. package/skills/ssh/README.md +140 -0
  299. package/skills/ssh/SKILL.md +135 -0
  300. package/skills/swift-linux/.plugin/plugin.json +17 -0
  301. package/skills/swift-linux/README.md +86 -0
  302. package/skills/swift-linux/SKILL.md +81 -0
  303. package/skills/theme-factory/.plugin/plugin.json +19 -0
  304. package/skills/theme-factory/LICENSE.txt +202 -0
  305. package/skills/theme-factory/README.md +58 -0
  306. package/skills/theme-factory/SKILL.md +59 -0
  307. package/skills/theme-factory/theme-showcase.pdf +0 -0
  308. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  309. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  310. package/skills/theme-factory/themes/desert-rose.md +19 -0
  311. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  312. package/skills/theme-factory/themes/golden-hour.md +19 -0
  313. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  314. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  315. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  316. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  317. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  318. package/skills/uv/.plugin/plugin.json +18 -0
  319. package/skills/uv/README.md +5 -0
  320. package/skills/uv/SKILL.md +95 -0
  321. package/skills/uv/references/README.md +5 -0
  322. package/skills/vercel/.plugin/plugin.json +18 -0
  323. package/skills/vercel/README.md +108 -0
  324. package/skills/vercel/SKILL.md +103 -0
  325. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  326. package/tests/test_catalogs.py +109 -0
  327. package/tests/test_code_review_risk_evaluation.py +94 -0
  328. package/tests/test_issue_duplicate_checker.py +240 -0
  329. package/tests/test_openhands_api_python.py +152 -0
  330. package/tests/test_plugin_manifest.py +83 -0
  331. package/tests/test_pr_review_diff_payload.py +202 -0
  332. package/tests/test_pr_review_feedback.py +263 -0
  333. package/tests/test_pr_review_prompt.py +152 -0
  334. package/tests/test_pr_review_review_context.py +253 -0
  335. package/tests/test_qa_changes.py +232 -0
  336. package/tests/test_qa_changes_evaluation.py +259 -0
  337. package/tests/test_release_notes_generator.py +990 -0
  338. package/tests/test_sdk_loading.py +150 -0
  339. package/tests/test_skill_plugin_loading.py +149 -0
  340. package/tests/test_skills_have_readme.py +66 -0
  341. package/tests/test_sync_extensions.py +292 -0
  342. package/tests/test_workflow_sync.py +46 -0
  343. package/utils/analysis/README.md +7 -0
  344. package/utils/analysis/laminar_signals/README.md +211 -0
  345. package/utils/analysis/laminar_signals/analyze.py +780 -0
  346. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  347. package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
@@ -0,0 +1,733 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Release Notes Generator
4
+
5
+ Generates consistent, well-structured release notes from git history.
6
+ Categorizes changes based on conventional commit prefixes and PR labels.
7
+
8
+ Environment Variables:
9
+ GITHUB_TOKEN: GitHub token for API access (required)
10
+ TAG: The release tag to generate notes for (required)
11
+ PREVIOUS_TAG: Override automatic detection of previous release (optional)
12
+ INCLUDE_INTERNAL: Include internal/infrastructure changes (default: false)
13
+ OUTPUT_FORMAT: Output format - 'release' or 'changelog' (default: release)
14
+ REPO_NAME: Repository name in format owner/repo (required)
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ import urllib.error
22
+ import urllib.parse
23
+ import urllib.request
24
+ from dataclasses import dataclass, field
25
+ from datetime import datetime, timezone
26
+ from typing import Any
27
+
28
+ # Category definitions with emojis and patterns
29
+ # Patterns match both conventional commit style (feat:, fix:) and bracket/paren style ([Feat]:, (Fix):)
30
+ CATEGORIES = {
31
+ "breaking": {
32
+ "emoji": "⚠️",
33
+ "title": "Breaking Changes",
34
+ "commit_patterns": [r"^BREAKING[\s\-:]", r"!:", r"^\[?BREAKING\]?[\s\-:]"],
35
+ "labels": ["breaking-change", "breaking"],
36
+ },
37
+ "features": {
38
+ "emoji": "✨",
39
+ "title": "New Features",
40
+ "commit_patterns": [
41
+ r"^feat(?:ure)?[\s\-:\(]",
42
+ r"^[\[\(]feat(?:ure)?[\]\)][\s\-:]*", # [Feat]: or (Feat):
43
+ ],
44
+ "labels": ["enhancement", "feature"],
45
+ },
46
+ "fixes": {
47
+ "emoji": "🐛",
48
+ "title": "Bug Fixes",
49
+ "commit_patterns": [
50
+ r"^fix(?:es)?[\s\-:\(]",
51
+ r"^bugfix[\s\-:\(]",
52
+ r"^[\[\(]fix(?:es)?[\]\)][\s\-:]*", # [Fix]: or (Fix):
53
+ r"^[\[\(]hotfix[\]\)][\s\-:]*", # [Hotfix]: or (Hotfix):
54
+ r"^hotfix[\s\-:\(]",
55
+ ],
56
+ "labels": ["bug", "bugfix"],
57
+ },
58
+ "docs": {
59
+ "emoji": "📚",
60
+ "title": "Documentation",
61
+ "commit_patterns": [
62
+ r"^docs?[\s\-:\(]",
63
+ r"^[\[\(]docs?[\]\)][\s\-:]*", # [Docs]: or (Docs):
64
+ ],
65
+ "labels": ["documentation", "docs"],
66
+ },
67
+ "internal": {
68
+ "emoji": "🏗️",
69
+ "title": "Internal/Infrastructure",
70
+ "commit_patterns": [
71
+ r"^chore[\s\-:\(]",
72
+ r"^ci[\s\-:\(]",
73
+ r"^refactor[\s\-:\(]",
74
+ r"^test[\s\-:\(]",
75
+ r"^build[\s\-:\(]",
76
+ r"^style[\s\-:\(]",
77
+ r"^perf[\s\-:\(]",
78
+ r"^[\[\(]chore[\]\)][\s\-:]*", # [Chore]: or (Chore):
79
+ r"^[\[\(]ci[\]\)][\s\-:]*",
80
+ r"^[\[\(]refactor[\]\)][\s\-:]*",
81
+ r"^[\[\(]test[\]\)][\s\-:]*",
82
+ ],
83
+ "labels": ["internal", "chore", "ci", "dependencies"],
84
+ },
85
+ }
86
+
87
+ KEYWORD_PATTERNS = {
88
+ "docs": [
89
+ r"\bdocs?\b",
90
+ r"\bdocumentation\b",
91
+ r"\breadme\b",
92
+ r"\bchangelog\b",
93
+ r"\bguide\b",
94
+ r"\bopenapi\b",
95
+ ],
96
+ "internal": [
97
+ r"\bci\b",
98
+ r"\blint\b",
99
+ r"\btyping\b",
100
+ r"\brefactor\b",
101
+ r"\bdebug\b",
102
+ r"\bpre-commit\b",
103
+ r"\bdockerfile\b",
104
+ r"\bdependencies?\b",
105
+ r"\brelease\b",
106
+ r"\btool descriptions?\b",
107
+ r"\bmicroagents?\b",
108
+ ],
109
+ "fixes": [
110
+ r"\bfix(?:es|ed)?\b",
111
+ r"\bbug\b",
112
+ r"\berror\b",
113
+ r"\bfail(?:ed|ing)?\b",
114
+ r"\bissue\b",
115
+ r"\bcrash\b",
116
+ r"\btimeout\b",
117
+ r"\bleak\b",
118
+ r"\bmissing\b",
119
+ r"\berroneous\b",
120
+ r"\breconnect\b",
121
+ r"\breset\b",
122
+ ],
123
+ "features": [
124
+ r"^(add|allow|support|enable|implement|introduce|create|provide|improve)\b",
125
+ ],
126
+ }
127
+
128
+
129
+ @dataclass
130
+ class Change:
131
+ """Represents a single change/commit in the release."""
132
+
133
+ message: str
134
+ sha: str
135
+ author: str
136
+ pr_number: int | None = None
137
+ pr_labels: list[str] = field(default_factory=list)
138
+ body: str = ""
139
+ url: str = ""
140
+ author_type: str = ""
141
+ pr_created_at: str = ""
142
+ pr_merged_at: str = ""
143
+ category: str = "other"
144
+
145
+ def to_markdown(self, repo_name: str) -> str:
146
+ """Format the change as a markdown list item."""
147
+ # Clean up the message - remove conventional commit prefix
148
+ # Supports multiple formats:
149
+ # - feat: message, fix(scope): message, feat!: breaking
150
+ # - [Feat]: message, (Fix): message, [Chore]: message
151
+ msg = self.message.strip()
152
+ for pattern in [
153
+ # Standard conventional commit: feat:, fix(scope):, feat!:
154
+ r"^(feat|fix|docs?|chore|ci|refactor|test|build|style|perf|BREAKING|hotfix)(\(.+?\))?!?:\s+",
155
+ # Bracket/paren style: [Feat]:, (Fix):, [Hotfix]:
156
+ r"^[\[\(](feat|fix|docs?|chore|ci|refactor|test|build|style|perf|BREAKING|hotfix)[\]\)][\s\-:]+",
157
+ ]:
158
+ msg = re.sub(pattern, "", msg, flags=re.IGNORECASE)
159
+ msg = msg.strip()
160
+
161
+ # Capitalize first letter
162
+ if msg:
163
+ msg = msg[0].upper() + msg[1:]
164
+
165
+ # Add PR link if available and not already in the message
166
+ if self.pr_number:
167
+ pr_ref = f"#{self.pr_number}"
168
+ if pr_ref not in msg:
169
+ msg += f" ({pr_ref})"
170
+
171
+ # Add author
172
+ if self.author:
173
+ msg += f" @{self.author}"
174
+
175
+ return f"- {msg}"
176
+
177
+
178
+ @dataclass
179
+ class Contributor:
180
+ """Represents a contributor to the release."""
181
+
182
+ username: str
183
+ first_pr: int | None = None
184
+ is_new: bool = False
185
+
186
+
187
+ @dataclass
188
+ class ReleaseNotes:
189
+ """Holds all data needed to generate release notes."""
190
+
191
+ tag: str
192
+ previous_tag: str
193
+ date: str
194
+ repo_name: str
195
+ commit_count: int = 0
196
+ changes: dict[str, list[Change]] = field(default_factory=dict)
197
+ contributors: list[Contributor] = field(default_factory=list)
198
+ new_contributors: list[Contributor] = field(default_factory=list)
199
+
200
+ def to_markdown(self, include_internal: bool = False) -> str:
201
+ """Generate the full release notes markdown."""
202
+ lines = [f"## [{self.tag}] - {self.date}", ""]
203
+
204
+ # Order of categories to display
205
+ category_order = ["breaking", "features", "fixes", "docs"]
206
+ if include_internal:
207
+ category_order.append("internal")
208
+
209
+ # Add categorized changes
210
+ for category in category_order:
211
+ changes = self.changes.get(category, [])
212
+ if changes:
213
+ cat_info = CATEGORIES[category]
214
+ lines.append(f"### {cat_info['emoji']} {cat_info['title']}")
215
+ for change in changes:
216
+ lines.append(change.to_markdown(self.repo_name))
217
+ lines.append("")
218
+
219
+ # Add new contributors section
220
+ if self.new_contributors:
221
+ lines.append("### 👥 New Contributors")
222
+ for contrib in self.new_contributors:
223
+ pr_text = f" in #{contrib.first_pr}" if contrib.first_pr else ""
224
+ lines.append(f"- @{contrib.username} made their first contribution{pr_text}")
225
+ lines.append("")
226
+
227
+ # Add full changelog link
228
+ lines.append(
229
+ f"**Full Changelog**: https://github.com/{self.repo_name}/compare/"
230
+ f"{self.previous_tag}...{self.tag}"
231
+ )
232
+
233
+ return "\n".join(lines)
234
+
235
+
236
+ def get_env(name: str, default: str | None = None, required: bool = False) -> str:
237
+ """Get an environment variable."""
238
+ value = os.getenv(name, default)
239
+ if required and not value:
240
+ print(f"Error: {name} environment variable is required")
241
+ sys.exit(1)
242
+ return value or ""
243
+
244
+
245
+ def github_api_request(
246
+ endpoint: str,
247
+ token: str,
248
+ method: str = "GET",
249
+ data: dict[str, Any] | None = None,
250
+ ) -> Any:
251
+ """Make a request to the GitHub API."""
252
+ url = f"https://api.github.com{endpoint}"
253
+ request = urllib.request.Request(url, method=method)
254
+ request.add_header("Accept", "application/vnd.github+json")
255
+ request.add_header("Authorization", f"Bearer {token}")
256
+ request.add_header("X-GitHub-Api-Version", "2022-11-28")
257
+
258
+ if data:
259
+ request.add_header("Content-Type", "application/json")
260
+ request.data = json.dumps(data).encode("utf-8")
261
+
262
+ try:
263
+ with urllib.request.urlopen(request, timeout=60) as response:
264
+ return json.loads(response.read().decode("utf-8"))
265
+ except urllib.error.HTTPError as e:
266
+ details = (e.read() or b"").decode("utf-8", errors="replace").strip()
267
+ raise RuntimeError(
268
+ f"GitHub API request failed: HTTP {e.code} {e.reason}. {details}"
269
+ ) from e
270
+
271
+
272
+ def get_tags(repo_name: str, token: str) -> list[dict[str, Any]]:
273
+ """Get all tags from the repository, sorted by creation date."""
274
+ tags = []
275
+ page = 1
276
+ per_page = 100
277
+
278
+ while True:
279
+ endpoint = f"/repos/{repo_name}/tags?per_page={per_page}&page={page}"
280
+ page_tags = github_api_request(endpoint, token)
281
+ if not page_tags:
282
+ break
283
+ tags.extend(page_tags)
284
+ if len(page_tags) < per_page:
285
+ break
286
+ page += 1
287
+
288
+ return tags
289
+
290
+
291
+ def find_previous_tag(
292
+ current_tag: str, tags: list[dict[str, Any]]
293
+ ) -> str | None:
294
+ """Find the previous release tag before the current one."""
295
+ # Filter to only semver tags (with optional pre-release/build metadata)
296
+ semver_pattern = re.compile(r"^v?\d+\.\d+\.\d+(?:[.-].*)?$")
297
+ semver_tags = [t for t in tags if semver_pattern.match(t["name"])]
298
+
299
+ # Find current tag index
300
+ current_idx = None
301
+ for i, tag in enumerate(semver_tags):
302
+ if tag["name"] == current_tag:
303
+ current_idx = i
304
+ break
305
+
306
+ if current_idx is None:
307
+ return None
308
+
309
+ # Return the next tag (which is the previous release since tags are sorted newest first)
310
+ if current_idx + 1 < len(semver_tags):
311
+ return semver_tags[current_idx + 1]["name"]
312
+
313
+ return None
314
+
315
+
316
+ def get_commits_between_tags(
317
+ repo_name: str, base_tag: str, head_tag: str, token: str
318
+ ) -> list[dict[str, Any]]:
319
+ """Get all commits between two tags."""
320
+ endpoint = f"/repos/{repo_name}/compare/{base_tag}...{head_tag}"
321
+ response = github_api_request(endpoint, token)
322
+ commits = response.get("commits", [])
323
+
324
+ total_commits = response.get("total_commits")
325
+ if isinstance(total_commits, int) and total_commits > len(commits):
326
+ print(
327
+ "Warning: GitHub compare API truncated the commit list; "
328
+ "release notes may be incomplete.",
329
+ file=sys.stderr,
330
+ )
331
+
332
+ return commits
333
+
334
+
335
+ def get_pr_for_commit(
336
+ repo_name: str, sha: str, token: str
337
+ ) -> dict[str, Any] | None:
338
+ """Get the PR associated with a commit (if any)."""
339
+ endpoint = f"/repos/{repo_name}/commits/{sha}/pulls"
340
+ try:
341
+ prs = github_api_request(endpoint, token)
342
+ if prs:
343
+ # Return the first merged PR
344
+ for pr in prs:
345
+ if pr.get("merged_at"):
346
+ return pr
347
+ # If no merged PR, return the first one
348
+ return prs[0]
349
+ except Exception as e:
350
+ # Log but don't fail - PR data is optional
351
+ print(f"Warning: Could not fetch PR for commit {sha[:7]}: {e}", file=sys.stderr)
352
+ return None
353
+
354
+
355
+ def _matches_any_pattern(text: str, patterns: list[str]) -> bool:
356
+ """Return True if the text matches any of the provided regex patterns."""
357
+ return any(re.search(pattern, text, re.IGNORECASE) for pattern in patterns)
358
+
359
+
360
+ def categorize_change(change: Change) -> str:
361
+ """Determine the category for a change based on commit message and PR labels."""
362
+ # Exact matches first: conventional commit prefixes are the strongest signal.
363
+ for category, info in CATEGORIES.items():
364
+ if _matches_any_pattern(change.message, info["commit_patterns"]):
365
+ return category
366
+
367
+ # Strong keyword matches help suppress noisy internal-only PRs even when a
368
+ # repository applies broad labels like `bug` or `enhancement`.
369
+ for category in ["docs", "internal"]:
370
+ if _matches_any_pattern(change.message, KEYWORD_PATTERNS[category]):
371
+ return category
372
+
373
+ label_names = [label.lower() for label in change.pr_labels]
374
+ for category, info in CATEGORIES.items():
375
+ if any(label.lower() in label_names for label in info["labels"]):
376
+ return category
377
+
378
+ # Fallback heuristics make PR-title based release notes more useful while
379
+ # still preferring user-facing categories over noisy implementation details.
380
+ for category in ["fixes", "features"]:
381
+ if _matches_any_pattern(change.message, KEYWORD_PATTERNS[category]):
382
+ return category
383
+
384
+ return "other"
385
+
386
+
387
+ def _parse_github_timestamp(timestamp: str) -> datetime:
388
+ """Parse a GitHub timestamp into a timezone-aware datetime."""
389
+ return datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
390
+
391
+
392
+ def _is_bot_author(author: str, author_type: str = "") -> bool:
393
+ """Return True when the contributor is clearly a bot account."""
394
+ normalized_type = author_type.lower()
395
+ return normalized_type == "bot" or author.endswith("[bot]")
396
+
397
+
398
+ def _search_merged_pull_requests_by_author(
399
+ repo_name: str, author: str, token: str
400
+ ) -> list[dict[str, Any]]:
401
+ """Return merged PRs in the repo authored by the given GitHub user."""
402
+ results: list[dict[str, Any]] = []
403
+ page = 1
404
+ query = f'repo:{repo_name} is:pr is:merged author:"{author}"'
405
+ encoded_query = urllib.parse.quote(query)
406
+
407
+ while True:
408
+ endpoint = (
409
+ f"/search/issues?q={encoded_query}&per_page=100&page={page}"
410
+ "&sort=created&order=asc"
411
+ )
412
+ response = github_api_request(endpoint, token)
413
+ items = response.get("items", [])
414
+ results.extend(items)
415
+ if len(items) < 100:
416
+ break
417
+ page += 1
418
+
419
+ return results
420
+
421
+
422
+ def is_new_contributor(
423
+ author: str,
424
+ repo_name: str,
425
+ before_timestamp: str,
426
+ token: str,
427
+ current_pr_number: int | None = None,
428
+ author_type: str = "",
429
+ ) -> bool:
430
+ """Check whether a human author's earliest release PR is their first merged PR."""
431
+ if not author or _is_bot_author(author, author_type) or not before_timestamp:
432
+ return False
433
+
434
+ threshold = _parse_github_timestamp(before_timestamp)
435
+
436
+ try:
437
+ for pr in _search_merged_pull_requests_by_author(repo_name, author, token):
438
+ if current_pr_number and pr.get("number") == current_pr_number:
439
+ continue
440
+ closed_at = pr.get("closed_at")
441
+ if not closed_at:
442
+ continue
443
+ if _parse_github_timestamp(closed_at) < threshold:
444
+ return False
445
+ return True
446
+ except Exception as e:
447
+ print(f"Warning: Could not check contributor history for {author}: {e}", file=sys.stderr)
448
+ return False
449
+
450
+
451
+ def get_tag_date(repo_name: str, tag: str, token: str) -> str:
452
+ """Get the date when a tag was created."""
453
+ endpoint = f"/repos/{repo_name}/git/refs/tags/{tag}"
454
+ try:
455
+ ref = github_api_request(endpoint, token)
456
+ # Get the commit or tag object
457
+ obj_sha = ref["object"]["sha"]
458
+ obj_type = ref["object"]["type"]
459
+
460
+ if obj_type == "tag":
461
+ # Annotated tag - get the tag object
462
+ tag_endpoint = f"/repos/{repo_name}/git/tags/{obj_sha}"
463
+ tag_obj = github_api_request(tag_endpoint, token)
464
+ date_str = tag_obj["tagger"]["date"]
465
+ else:
466
+ # Lightweight tag - get the commit
467
+ commit_endpoint = f"/repos/{repo_name}/git/commits/{obj_sha}"
468
+ commit_obj = github_api_request(commit_endpoint, token)
469
+ date_str = commit_obj["committer"]["date"]
470
+
471
+ # Parse and format the date
472
+ dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
473
+ return dt.strftime("%Y-%m-%d")
474
+ except Exception as e:
475
+ # Log but fall back to current date
476
+ print(f"Warning: Could not get tag date for {tag}: {e}", file=sys.stderr)
477
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d")
478
+
479
+
480
+ def _process_commit(
481
+ commit_data: dict[str, Any], repo_name: str, token: str
482
+ ) -> Change | None:
483
+ """Process a single commit into a Change object."""
484
+ sha = commit_data["sha"]
485
+ message = commit_data["commit"]["message"].split("\n")[0] # First line only
486
+ author = commit_data.get("author", {}).get("login", "")
487
+
488
+ # Skip merge commits
489
+ if message.lower().startswith("merge "):
490
+ return None
491
+
492
+ # Get PR info
493
+ pr_number = None
494
+ pr_labels: list[str] = []
495
+ pr = get_pr_for_commit(repo_name, sha, token)
496
+ body = ""
497
+ url = ""
498
+ author_type = ""
499
+ pr_created_at = ""
500
+ pr_merged_at = ""
501
+ if pr:
502
+ pr_number = pr["number"]
503
+ pr_labels = [label["name"] for label in pr.get("labels", [])]
504
+ author = pr.get("user", {}).get("login", "") or author
505
+ author_type = pr.get("user", {}).get("type", "") or ""
506
+ message = pr.get("title") or message
507
+ body = pr.get("body") or ""
508
+ url = pr.get("html_url") or ""
509
+ pr_created_at = pr.get("created_at") or ""
510
+ pr_merged_at = pr.get("merged_at") or ""
511
+
512
+ return Change(
513
+ message=message,
514
+ sha=sha,
515
+ author=author,
516
+ pr_number=pr_number,
517
+ pr_labels=pr_labels,
518
+ body=body,
519
+ url=url,
520
+ author_type=author_type,
521
+ pr_created_at=pr_created_at,
522
+ pr_merged_at=pr_merged_at,
523
+ )
524
+
525
+
526
+ def _dedupe_changes(changes_list: list[Change]) -> list[Change]:
527
+ """Collapse multiple commits from the same PR into one release-note entry."""
528
+ deduped: list[Change] = []
529
+ seen_keys: set[str] = set()
530
+
531
+ for change in changes_list:
532
+ key = f"pr:{change.pr_number}" if change.pr_number else f"sha:{change.sha}"
533
+ if key in seen_keys:
534
+ continue
535
+ seen_keys.add(key)
536
+ deduped.append(change)
537
+
538
+ return deduped
539
+
540
+
541
+ def _categorize_changes(
542
+ changes_list: list[Change],
543
+ ) -> dict[str, list[Change]]:
544
+ """Categorize a list of changes by type."""
545
+ categorized: dict[str, list[Change]] = {cat: [] for cat in CATEGORIES}
546
+ categorized["other"] = []
547
+
548
+ for change in changes_list:
549
+ change.category = categorize_change(change)
550
+ if change.category in categorized:
551
+ categorized[change.category].append(change)
552
+ else:
553
+ categorized["other"].append(change)
554
+
555
+ return categorized
556
+
557
+
558
+ def _process_contributors(
559
+ changes_list: list[Change],
560
+ repo_name: str,
561
+ token: str,
562
+ ) -> tuple[list[Contributor], list[Contributor]]:
563
+ """Extract contributors and identify first-time human PR contributors."""
564
+ contributors: dict[str, Contributor] = {}
565
+ earliest_pr_by_author: dict[str, Change] = {}
566
+ new_contributors: list[Contributor] = []
567
+
568
+ for change in changes_list:
569
+ author = change.author
570
+ if not author:
571
+ continue
572
+
573
+ contrib = contributors.get(author)
574
+ if contrib is None:
575
+ contrib = Contributor(username=author, first_pr=change.pr_number)
576
+ contributors[author] = contrib
577
+ elif contrib.first_pr is None and change.pr_number:
578
+ contrib.first_pr = change.pr_number
579
+
580
+ if not change.pr_number:
581
+ continue
582
+
583
+ candidate_timestamp = change.pr_merged_at or change.pr_created_at
584
+ current = earliest_pr_by_author.get(author)
585
+ current_timestamp = (current.pr_merged_at or current.pr_created_at) if current else ""
586
+ if current is None or (
587
+ candidate_timestamp
588
+ and (not current_timestamp or candidate_timestamp < current_timestamp)
589
+ ):
590
+ earliest_pr_by_author[author] = change
591
+ contrib.first_pr = change.pr_number
592
+
593
+ for author, contrib in contributors.items():
594
+ first_pr_change = earliest_pr_by_author.get(author)
595
+ if not first_pr_change:
596
+ continue
597
+
598
+ first_pr_timestamp = first_pr_change.pr_merged_at or first_pr_change.pr_created_at
599
+ if is_new_contributor(
600
+ author,
601
+ repo_name,
602
+ first_pr_timestamp,
603
+ token,
604
+ current_pr_number=first_pr_change.pr_number,
605
+ author_type=first_pr_change.author_type,
606
+ ):
607
+ contrib.is_new = True
608
+ new_contributors.append(contrib)
609
+
610
+ return list(contributors.values()), new_contributors
611
+
612
+
613
+ def generate_release_notes(
614
+ tag: str,
615
+ previous_tag: str | None,
616
+ repo_name: str,
617
+ token: str,
618
+ include_internal: bool = False,
619
+ ) -> ReleaseNotes:
620
+ """Generate release notes for the given tag."""
621
+ # Get all tags
622
+ tags = get_tags(repo_name, token)
623
+
624
+ # Find previous tag if not provided
625
+ if not previous_tag:
626
+ previous_tag = find_previous_tag(tag, tags)
627
+
628
+ if not previous_tag:
629
+ print(f"Warning: Could not find previous tag for {tag}")
630
+ previous_tag = tags[-1]["name"] if tags else "HEAD~100"
631
+
632
+ print(f"Generating release notes: {previous_tag} -> {tag}")
633
+
634
+ # Get tag date
635
+ tag_date = get_tag_date(repo_name, tag, token)
636
+
637
+ # Get commits between tags
638
+ commits = get_commits_between_tags(repo_name, previous_tag, tag, token)
639
+ print(f"Found {len(commits)} commits")
640
+
641
+ # Phase 1: Process commits into Change objects
642
+ raw_changes = [
643
+ change
644
+ for c in commits
645
+ if (change := _process_commit(c, repo_name, token)) is not None
646
+ ]
647
+
648
+ # Phase 2: Collapse multiple commits from the same PR into a single entry.
649
+ changes = _dedupe_changes(raw_changes)
650
+
651
+ # Phase 3: Categorize changes
652
+ categorized = _categorize_changes(changes)
653
+
654
+ # Phase 4: Extract and identify contributors
655
+ contributors, new_contributors = _process_contributors(changes, repo_name, token)
656
+
657
+ return ReleaseNotes(
658
+ tag=tag,
659
+ previous_tag=previous_tag,
660
+ date=tag_date,
661
+ repo_name=repo_name,
662
+ commit_count=len(commits),
663
+ changes=categorized,
664
+ contributors=contributors,
665
+ new_contributors=new_contributors,
666
+ )
667
+
668
+
669
+ def set_github_output(name: str, value: str) -> None:
670
+ """Set a GitHub Actions output variable."""
671
+ output_file = os.getenv("GITHUB_OUTPUT")
672
+ if output_file:
673
+ with open(output_file, "a") as f:
674
+ # Handle multiline values
675
+ if "\n" in value:
676
+ delimiter = f"EOF_{os.urandom(4).hex()}"
677
+ f.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n")
678
+ else:
679
+ f.write(f"{name}={value}\n")
680
+ else:
681
+ print(f"::set-output name={name}::{value}")
682
+
683
+
684
+ def main():
685
+ """Main entry point."""
686
+ # Get configuration from environment
687
+ token = get_env("GITHUB_TOKEN", required=True)
688
+ tag = get_env("TAG", required=True)
689
+ previous_tag = get_env("PREVIOUS_TAG") or None
690
+ include_internal = get_env("INCLUDE_INTERNAL", "false").lower() == "true"
691
+ output_format = get_env("OUTPUT_FORMAT", "release")
692
+ repo_name = get_env("REPO_NAME", required=True)
693
+
694
+ print(f"Generating release notes for {repo_name}")
695
+ print(f"Tag: {tag}")
696
+ print(f"Previous tag: {previous_tag or 'auto-detect'}")
697
+ print(f"Include internal: {include_internal}")
698
+ print(f"Output format: {output_format}")
699
+
700
+ # Generate release notes
701
+ notes = generate_release_notes(
702
+ tag=tag,
703
+ previous_tag=previous_tag,
704
+ repo_name=repo_name,
705
+ token=token,
706
+ include_internal=include_internal,
707
+ )
708
+
709
+ # Generate markdown
710
+ markdown = notes.to_markdown(include_internal=include_internal)
711
+
712
+ # Write to file
713
+ with open("release_notes.md", "w") as f:
714
+ f.write(markdown)
715
+
716
+ print("\n" + "=" * 60)
717
+ print("Generated Release Notes:")
718
+ print("=" * 60)
719
+ print(markdown)
720
+ print("=" * 60)
721
+
722
+ # Set GitHub Actions outputs
723
+ set_github_output("release_notes", markdown)
724
+ set_github_output("previous_tag", notes.previous_tag)
725
+ set_github_output("commit_count", str(notes.commit_count))
726
+ set_github_output("contributor_count", str(len(notes.contributors)))
727
+ set_github_output("new_contributor_count", str(len(notes.new_contributors)))
728
+
729
+ print("\nRelease notes generated successfully!")
730
+
731
+
732
+ if __name__ == "__main__":
733
+ main()