release.py
1 #!/usr/bin/env python3 2 """Hermes Agent Release Script 3 4 Generates changelogs and creates GitHub releases with CalVer tags. 5 6 Usage: 7 # Preview changelog (dry run) 8 python scripts/release.py 9 10 # Preview with semver bump 11 python scripts/release.py --bump minor 12 13 # Create the release 14 python scripts/release.py --bump minor --publish 15 16 # First release (no previous tag) 17 python scripts/release.py --bump minor --publish --first-release 18 19 # Override CalVer date (e.g. for a belated release) 20 python scripts/release.py --bump minor --publish --date 2026.3.15 21 """ 22 23 import argparse 24 import re 25 import shutil 26 import subprocess 27 import sys 28 from collections import defaultdict 29 from datetime import datetime 30 from pathlib import Path 31 32 REPO_ROOT = Path(__file__).resolve().parent.parent 33 VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py" 34 PYPROJECT_FILE = REPO_ROOT / "pyproject.toml" 35 36 # ────────────────────────────────────────────────────────────────────── 37 # Git email → GitHub username mapping 38 # ────────────────────────────────────────────────────────────────────── 39 40 # Auto-extracted from noreply emails + manual overrides 41 AUTHOR_MAP = { 42 # teknium (multiple emails) 43 "teknium1@gmail.com": "teknium1", 44 "m@mobrienv.dev": "mikeyobrien", 45 "qiyin.zuo@pcitc.com": "qiyin-code", 46 "leone.parise@gmail.com": "leoneparise", 47 "teknium@nousresearch.com": "teknium1", 48 "127238744+teknium1@users.noreply.github.com": "teknium1", 49 "159539633+MottledShadow@users.noreply.github.com": "MottledShadow", 50 "aludwin+gh@gmail.com": "adamludwin", 51 "ngusev@astralinux.ru": "NikolayGusev-astra", 52 "2093036+exiao@users.noreply.github.com": "exiao", 53 "rylen.anil@gmail.com": "rylena", 54 "godnanijatin@gmail.com": "jatingodnani", 55 "14046872+tmimmanuel@users.noreply.github.com": "tmimmanuel", 56 "657290301@qq.com": "IMHaoyan", 57 "revar@users.noreply.github.com": "revaraver", 58 # Matrix parity salvage batch (April 2026) 59 "sr@samirusani": "samrusani", 60 "angelclaw@AngelMacBook.local": "angel12", 61 "charles@cryptoassetrecovery.com": "charles-brooks", 62 # DeepSeek v4 + Kimi thinking-mode reasoning_content salvage (April 2026) 63 "luwinyang@deepseek.com": "lsdsjy", 64 "season.saw@gmail.com": "season179", 65 "heathley@Heathley-MacBook-Air.local": "heathley", 66 "vlad19@gmail.com": "dandaka", 67 "adamrummer@gmail.com": "cyclingwithelephants", 68 "nbot@liizfq.top": "liizfq", 69 "274096618+hermes-agent-dhabibi@users.noreply.github.com": "dhabibi", 70 "dejie.guo@gmail.com": "JayGwod", 71 "133716830+0xKingBack@users.noreply.github.com": "0xKingBack", 72 "daixin1204@gmail.com": "SimbaKingjoe", 73 "maxence@groine.fr": "MaxyMoos", 74 "61830395+leprincep35700@users.noreply.github.com": "leprincep35700", 75 # OpenViking viking_read salvage (April 2026) 76 "hitesh@gmail.com": "htsh", 77 "pty819@outlook.com": "pty819", 78 "pty819@users.noreply.github.com": "pty819", 79 "517024110@qq.com": "chennest", 80 # Curator fixes (Apr 30 2026) 81 "yuxiangl490@gmail.com": "y0shua1ee", 82 "manmit0x@gmail.com": "0xDevNinja", 83 "aamirjawaid@microsoft.com": "heyitsaamir", 84 "johnnncenaaa77@gmail.com": "johnncenae", 85 "thomasjhon6666@gmail.com": "ThomassJonax", 86 "focusflow.app.help@gmail.com": "yes999zc", 87 "rob@atlas.lan": "rmoen", 88 # Slack ephemeral slash-ack salvage (May 2026) 89 "probepark@users.noreply.github.com": "probepark", 90 # Slack batch salvage (May 2026) 91 "280484231+prive-fe-bot@users.noreply.github.com": "priveperfumes", 92 "amr@ghanem.sa": "amroessam", 93 "paperlantern.agent@gmail.com": "Hinotoi-agent", 94 "valda@underscore.jp": "valda", 95 "162235745+0z1-ghb@users.noreply.github.com": "0z1-ghb", 96 "yes999zc@163.com": "yes999zc", 97 "343873859@qq.com": "DrStrangerUJN", 98 "252818347@qq.com": "hejuntt1014", 99 "uzmpsk.dilekakbas@gmail.com": "dlkakbs", 100 "beliefanx@gmail.com": "BeliefanX", 101 "changchun989@proton.me": "changchun989", 102 "jefferson@heimdallstrategy.com": "Mind-Dragon", 103 "44753291+Nanako0129@users.noreply.github.com": "Nanako0129", 104 "steve.westerhouse@origami-analytics.com": "westers", 105 "yeyitech@users.noreply.github.com": "yeyitech", 106 "260878550+beenherebefore@users.noreply.github.com": "beenherebefore", 107 "79389617+txbxxx@users.noreply.github.com": "txbxxx", 108 "liuhao03@bilibili.com": "liuhao1024", 109 "130918800+devorun@users.noreply.github.com": "devorun", 110 "surat.s@itm.kmutnb.ac.th": "beesrsj2500", 111 "beesr@bee.localdomain": "beesrsj2500", 112 "mind-dragon@nous.research": "Mind-Dragon", 113 "juntingpublic@gmail.com": "JustinUssuri", 114 "mtf201013@gmail.com": "ma-pony", 115 "sonoyuncudmr@gmail.com": "Sonoyunchu", 116 "43525405+yatesjalex@users.noreply.github.com": "yatesjalex", 117 "maks.mir@yahoo.com": "say8hi", 118 "27719690+Mirac1eSky@users.noreply.github.com": "Mirac1eSky", 119 "web3blind@users.noreply.github.com": "web3blind", 120 "julia@alexland.us": "alexg0bot", 121 "christian@scheid.tech": "scheidti", 122 # Moonshot schema anyOf+enum salvage (May 2026) 123 "git@local.invalid": "hendrixfreire", 124 "1060770+benjaminsehl@users.noreply.github.com": "benjaminsehl", 125 "nerijusn76@gmail.com": "Nerijusas", 126 "itonov@proton.me": "Ito-69", 127 "glesstech@gmail.com": "georgeglessner", 128 "maxim.smetanin@gmail.com": "maxims-oss", 129 "nazirulhafiy@gmail.com": "nazirulhafiy", 130 "CREWorx@users.noreply.github.com": "BadTechBandit", 131 "yoimexex@gmail.com": "Yoimex", 132 "6548898+romanornr@users.noreply.github.com": "romanornr", 133 "foxion37@gmail.com": "foxion37", 134 "bloodcarter@gmail.com": "bloodcarter", 135 "scott@scotttrinh.com": "scotttrinh", 136 "quocanh261997@gmail.com": "quocanh261997", 137 # contributors (from noreply pattern) 138 "david.vv@icloud.com": "davidvv", 139 "wangqiang@wangqiangdeMac-mini.local": "xiaoqiang243", 140 "snreynolds2506@gmail.com": "snreynolds", 141 "35742124+0xbyt4@users.noreply.github.com": "0xbyt4", 142 "71184274+MassiveMassimo@users.noreply.github.com": "MassiveMassimo", 143 "massivemassimo@users.noreply.github.com": "MassiveMassimo", 144 "82637225+kshitijk4poor@users.noreply.github.com": "kshitijk4poor", 145 "keifergu@tencent.com": "keifergu", 146 "kshitijk4poor@users.noreply.github.com": "kshitijk4poor", 147 "SHL0MS@users.noreply.github.com": "SHL0MS", 148 "abner.the.foreman@agentmail.to": "Abnertheforeman", 149 "adam.manning@pro-serveinc.com": "amanning3390", 150 "thomasgeorgevii09@gmail.com": "tochukwuada", 151 "sb@wmc.sh": "zicochaos", 152 "harryykyle1@gmail.com": "hharry11", 153 "kshitijk4poor@gmail.com": "kshitijk4poor", 154 "1294707+Tosko4@users.noreply.github.com": "Tosko4", 155 "keira.voss94@gmail.com": "keiravoss94", 156 "16443023+stablegenius49@users.noreply.github.com": "stablegenius49", 157 "fqsy1416@gmail.com": "EKKOLearnAI", 158 "octo-patch@github.com": "octo-patch", 159 "math0r-be@github.com": "math0r-be", 160 "simbamax99@gmail.com": "simbam99", 161 "iris@growthpillars.co": "irispillars", 162 "185121704+stablegenius49@users.noreply.github.com": "stablegenius49", 163 "101283333+batuhankocyigit@users.noreply.github.com": "batuhankocyigit", 164 "255305877+ismell0992-afk@users.noreply.github.com": "ismell0992-afk", 165 "cyprian@ironin.pl": "iRonin", 166 "valdi.jorge@gmail.com": "jvcl", 167 "q19dcp@gmail.com": "aj-nt", 168 "ebukau84@gmail.com": "UgwujaGeorge", 169 "francip@gmail.com": "francip", 170 "omni@comelse.com": "omnissiah-comelse", 171 "oussama.redcode@gmail.com": "mavrickdeveloper", 172 "126368201+vilkasdev@users.noreply.github.com": "vilkasdev", 173 "137614867+cutepawss@users.noreply.github.com": "cutepawss", 174 "96793918+memosr@users.noreply.github.com": "memosr", 175 "mehmet.sr35@gmail.com": "memosr", 176 "milkoor@users.noreply.github.com": "milkoor", 177 "xuerui911@gmail.com": "Fatty911", 178 "131039422+SHL0MS@users.noreply.github.com": "SHL0MS", 179 "77628552+raulvidis@users.noreply.github.com": "raulvidis", 180 "145567217+Aum08Desai@users.noreply.github.com": "Aum08Desai", 181 "256820943+kshitij-eliza@users.noreply.github.com": "kshitij-eliza", 182 "jiechengwu@pony.ai": "Jason2031", 183 "44278268+shitcoinsherpa@users.noreply.github.com": "shitcoinsherpa", 184 "104278804+Sertug17@users.noreply.github.com": "Sertug17", 185 "112503481+caentzminger@users.noreply.github.com": "caentzminger", 186 "258577966+voidborne-d@users.noreply.github.com": "voidborne-d", 187 "liusway405@gmail.com": "voidborne-d", 188 "xydarcher@uestc.edu.cn": "Readon", 189 "sir_even@icloud.com": "sirEven", 190 "36056348+sirEven@users.noreply.github.com": "sirEven", 191 "70424851+insecurejezza@users.noreply.github.com": "insecurejezza", 192 "jezzahehn@gmail.com": "JezzaHehn", 193 "254021826+dodo-reach@users.noreply.github.com": "dodo-reach", 194 "259807879+Bartok9@users.noreply.github.com": "Bartok9", 195 "270082434+crayfish-ai@users.noreply.github.com": "crayfish-ai", 196 "241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter", 197 "268667990+Roy-oss1@users.noreply.github.com": "Roy-oss1", 198 "27917469+nosleepcassette@users.noreply.github.com": "nosleepcassette", 199 "241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter", 200 "109555139+davetist@users.noreply.github.com": "davetist", 201 "39405770+yyq4193@users.noreply.github.com": "yyq4193", 202 "Asunfly@users.noreply.github.com": "Asunfly", 203 "2500400+honghua@users.noreply.github.com": "honghua", 204 "462836+jplew@users.noreply.github.com": "jplew", 205 "nish3451@users.noreply.github.com": "nish3451", 206 "Mibayy@users.noreply.github.com": "Mibayy", 207 "mibayy@users.noreply.github.com": "Mibayy", 208 "135070653+sgaofen@users.noreply.github.com": "sgaofen", 209 "lzy.dev@gmail.com": "zhiyanliu", 210 "me@janstepanovsky.cz": "hhhonzik", 211 "139848623+hhuang91@users.noreply.github.com": "hhuang91", 212 "s.ozaki@ebinou.net": "Satoshi-agi", 213 "10774721+kunlabs@users.noreply.github.com": "kunlabs", 214 "110560187+Wang-tianhao@users.noreply.github.com": "Wang-tianhao", 215 "170458616+ghostmfr@users.noreply.github.com": "ghostmfr", 216 "1848670+mewwts@users.noreply.github.com": "mewwts", 217 "1930707+haru398801@users.noreply.github.com": "haru398801", 218 "rapabelias@gmail.com": "badgerbees", 219 "xnb888@proton.me": "xnbi", 220 "xiahu889889@proton.me": "xiahu88988", 221 "nocoo@users.noreply.github.com": "nocoo", 222 "30841158+n-WN@users.noreply.github.com": "n-WN", 223 "tsuijinglei@gmail.com": "hiddenpuppy", 224 "buraysandro9@gmail.com": "ygd58", 225 "jerome@clawwork.ai": "HiddenPuppy", 226 "jerome.benoit@sap.com": "jerome-benoit", 227 "wysie@users.noreply.github.com": "Wysie", 228 "leoyuan0099@gmail.com": "keyuyuan", 229 "bxzt2006@163.com": "Only-Code-A", 230 "i@troy-y.org": "TroyMitchell911", 231 "mygamez@163.com": "zhongyueming1121", 232 "hansnow@users.noreply.github.com": "hansnow", 233 "134848055+UNLINEARITY@users.noreply.github.com": "UNLINEARITY", 234 "ben.burtenshaw@gmail.com": "burtenshaw", 235 "roopaknijhara@gmail.com": "rnijhara", 236 "josephzcan@gmail.com": "j0sephz", 237 # contributors (manual mapping from git names) 238 "ahmedsherif95@gmail.com": "asheriif", 239 "dyxushuai@gmail.com": "dyxushuai", 240 "33860762+etcircle@users.noreply.github.com": "etcircle", 241 "liujinkun@bytedance.com": "liujinkun2025", 242 "dmayhem93@gmail.com": "dmahan93", 243 "fr@tecompanytea.com": "ifrederico", 244 "cdanis@gmail.com": "cdanis", 245 "samherring99@gmail.com": "samherring99", 246 "desaiaum08@gmail.com": "Aum08Desai", 247 "shannon.sands.1979@gmail.com": "shannonsands", 248 "shannon@nousresearch.com": "shannonsands", 249 "abdi.moya@gmail.com": "AxDSan", 250 "eri@plasticlabs.ai": "Erosika", 251 "hjcpuro@gmail.com": "hjc-puro", 252 "xaydinoktay@gmail.com": "aydnOktay", 253 "abdullahfarukozden@gmail.com": "Farukest", 254 "lovre.pesut@gmail.com": "rovle", 255 "xjtumj@gmail.com": "mengjian-github", 256 "kevinskysunny@gmail.com": "kevinskysunny", 257 "xiewenxuan462@gmail.com": "yule975", 258 "yiweimeng.dlut@hotmail.com": "meng93", 259 "hakanerten02@hotmail.com": "teyrebaz33", 260 "linux2010@users.noreply.github.com": "Linux2010", 261 "elmatadorgh@users.noreply.github.com": "elmatadorgh", 262 "alexazzjjtt@163.com": "alexzhu0", 263 "1180176+Swift42@users.noreply.github.com": "Swift42", 264 "ruzzgarcn@gmail.com": "Ruzzgar", 265 "yukipukikedy@gmail.com": "Yukipukii1", 266 "alireza78.crypto@gmail.com": "alireza78a", 267 "brooklyn.bb.nicholson@gmail.com": "brooklynnicholson", 268 "withapurpose37@gmail.com": "StefanIsMe", 269 "4317663+helix4u@users.noreply.github.com": "helix4u", 270 "ifkellx@users.noreply.github.com": "Ifkellx", 271 "331214+counterposition@users.noreply.github.com": "counterposition", 272 "blspear@gmail.com": "BrennerSpear", 273 "akhater@gmail.com": "akhater", 274 "Cos_Admin@PTG-COS.lodluvup4uaudnm3ycd14giyug.xx.internal.cloudapp.net": "akhater", 275 "239876380+handsdiff@users.noreply.github.com": "handsdiff", 276 "hesapacicam112@gmail.com": "etherman-os", 277 "mark.ramsell@rivermounts.com": "mark-ramsell", 278 "taeng02@icloud.com": "taeng0204", 279 "gpickett00@gmail.com": "gpickett00", 280 "mcosma@gmail.com": "wakamex", 281 "clawdia.nash@proton.me": "clawdia-nash", 282 "pickett.austin@gmail.com": "austinpickett", 283 "dangtc94@gmail.com": "dieutx", 284 "jaisehgal11299@gmail.com": "jaisup", 285 "percydikec@gmail.com": "PercyDikec", 286 "noonou7@gmail.com": "HenkDz", 287 # Azure Foundry salvage (PRs #9029, #4599, #10086, #8766) 288 "tech@smartlogics.net": "TechPrototyper", 289 "637186+HangGlidersRule@users.noreply.github.com": "HangGlidersRule", 290 "pein892@gmail.com": "pein892", 291 "dean.kerr@gmail.com": "deankerr", 292 "socrates1024@gmail.com": "socrates1024", 293 "seanalt555@gmail.com": "Salt-555", 294 "satelerd@gmail.com": "satelerd", 295 "dan@danlynn.com": "danklynn", 296 "mattmaximo@hotmail.com": "MattMaximo", 297 "MatthewRHardwick@gmail.com": "mrhwick", 298 "149063006+j3ffffff@users.noreply.github.com": "j3ffffff", 299 "A-FdL-Prog@users.noreply.github.com": "A-FdL-Prog", 300 "l0hde@users.noreply.github.com": "l0hde", 301 "difujia@users.noreply.github.com": "difujia", 302 "vominh1919@gmail.com": "vominh1919", 303 "yue.gu2023@gmail.com": "YueLich", 304 "51783311+andyylin@users.noreply.github.com": "andyylin", 305 "me@jakubkrcmar.cz": "jakubkrcmar", 306 "prasadus92@gmail.com": "prasadus92", 307 "michael@make.software": "mssteuer", 308 "der@konsi.org": "konsisumer", 309 "abogale2@gmail.com": "amanuel2", 310 "alexazzjjtt@163.com": "alexzhu0", 311 "pub_forgreatagent@antgroup.com": "AntAISecurityLab", 312 "252620095+briandevans@users.noreply.github.com": "briandevans", 313 "danielrpike9@gmail.com": "Bartok9", 314 "skozyuk@cruxexperts.com": "CruxExperts", 315 "154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43", 316 "12250313+Kailigithub@users.noreply.github.com": "Kailigithub", 317 "mgparkprint@gmail.com": "vlwkaos", 318 "1317078257maroon@gmail.com": "Oxidane-bot", 319 "tranquil_flow@protonmail.com": "Tranquil-Flow", 320 "LyleLengyel@gmail.com": "mcndjxlefnd", 321 "wangshengyang2004@163.com": "Wangshengyang2004", 322 "hasan.ali13381@gmail.com": "H-Ali13381", 323 "xienb@proton.me": "XieNBi", 324 "139681654+maymuneth@users.noreply.github.com": "maymuneth", 325 "zengwei@nightq.cn": "nightq", 326 "1434494126@qq.com": "5park1e", 327 "158153005+5park1e@users.noreply.github.com": "5park1e", 328 "innocarpe@gmail.com": "innocarpe", 329 "noreply@ked.com": "qike-ms", 330 "andrekurait@gmail.com": "AndreKurait", 331 "bsgdigital@users.noreply.github.com": "bsgdigital", 332 "numman.ali@gmail.com": "nummanali", 333 "rohithsaimidigudla@gmail.com": "whitehatjr1001", 334 "0xNyk@users.noreply.github.com": "0xNyk", 335 "0xnykcd@googlemail.com": "0xNyk", 336 "buraysandro9@gmail.com": "buray", 337 "contact@jomar.fr": "joshmartinelle", 338 "camilo@tekelala.com": "tekelala", 339 "vincentcharlebois@gmail.com": "vincentcharlebois", 340 "aryan@synvoid.com": "aryansingh", 341 "johnsonblake1@gmail.com": "blakejohnson", 342 "hcn518@gmail.com": "pedh", 343 "haileymarshall005@gmail.com": "haileymarshall", 344 "greer.guthrie@gmail.com": "g-guthrie", 345 "kennyx102@gmail.com": "bobashopcashier", 346 "77253505+bobashopcashier@users.noreply.github.com": "bobashopcashier", 347 "25355950+megastary@users.noreply.github.com": "megastary", # PR #18325 348 "shokatalishaikh95@gmail.com": "areu01or00", 349 "bryan@intertwinesys.com": "bryanyoung", 350 "christo.mitov@gmail.com": "christomitov", 351 "hermes@nousresearch.com": "NousResearch", 352 "reginaldasr@gmail.com": "ReginaldasR", 353 "ntconguit@gmail.com": "0xharryriddle", 354 "agent@wildcat.local": "ericnicolaides", 355 "georgex8001@gmail.com": "georgex8001", 356 "stefan@dimagents.ai": "dimitrovi", 357 "hermes@noushq.ai": "benbarclay", 358 "chinmingcock@gmail.com": "ChimingLiu", 359 "allard.quek@singtel.com": "AllardQuek", 360 "openclaw@sparklab.ai": "openclaw", 361 "semihcvlk53@gmail.com": "Himess", 362 "erenkar950@gmail.com": "erenkarakus", 363 "adavyasharma@gmail.com": "adavyas", 364 "acaayush1111@gmail.com": "aayushchaudhary", 365 "jason@outland.art": "jasonoutland", 366 "73175452+Magaav@users.noreply.github.com": "Magaav", 367 "mrflu1918@proton.me": "SPANISHFLU", 368 "morganemoss@gmai.com": "mormio", 369 "kopjop926@gmail.com": "cesareth", 370 "fuleinist@gmail.com": "fuleinist", 371 "jack.47@gmail.com": "JackTheGit", 372 "dalvidjr2022@gmail.com": "Jr-kenny", 373 "m@statecraft.systems": "mbierling", 374 "balyan.sid@gmail.com": "alt-glitch", 375 "52913345+alt-glitch@users.noreply.github.com": "alt-glitch", 376 "oluwadareab12@gmail.com": "bennytimz", 377 "simon@simonmarcus.org": "simon-marcus", 378 "xowiekk@gmail.com": "Xowiek", 379 "1243352777@qq.com": "zons-zhaozhy", 380 "e.silacandmr@gmail.com": "Es1la", 381 "h3057183414@gmail.com": "CoreyNoDream", 382 "franksong2702@gmail.com": "franksong2702", 383 "673088860@qq.com": "ambition0802", 384 "beibei1988@proton.me": "beibi9966", 385 # ── bulk addition: 75 emails resolved via API, PR salvage bodies, noreply 386 # crossref, and GH contributor list matching (April 2026 audit) ── 387 "1115117931@qq.com": "aaronagent", 388 "1506751656@qq.com": "hqhq1025", 389 "364939526@qq.com": "luyao618", 390 "hgk324@gmail.com": "houziershi", 391 "176644217+PStarH@users.noreply.github.com": "PStarH", 392 "51058514+Sanjays2402@users.noreply.github.com": "Sanjays2402", 393 "16577466+andy825@user.noreply.gitee.com": "Andy283", 394 "906014227@qq.com": "bingo906", 395 "aaronwong1999@icloud.com": "AaronWong1999", 396 "agents@kylefrench.dev": "DeployFaith", 397 "angelos@oikos.lan.home.malaiwah.com": "angelos", 398 "aptx4561@gmail.com": "cokemine", 399 "arilotter@gmail.com": "ethernet8023", 400 "ben@nousresearch.com": "benbarclay", 401 "birdiegyal@gmail.com": "yyovil", 402 "boschi1997@gmail.com": "nicoloboschi", 403 "chef.ya@gmail.com": "cherifya", 404 "chlqhdtn98@gmail.com": "BongSuCHOI", 405 "coffeemjj@gmail.com": "Cafexss", 406 "dalianmao0107@gmail.com": "dalianmao000", 407 "der@konsi.org": "konsisumer", 408 "dgrieco@redhat.com": "DomGrieco", 409 "dhicham.pro@gmail.com": "spideystreet", 410 "dipp.who@gmail.com": "dippwho", 411 "don.rhm@gmail.com": "donrhmexe", 412 "dorukardahan@hotmail.com": "dorukardahan", 413 "dsocolobsky@gmail.com": "dsocolobsky", 414 "dylan.socolobsky@lambdaclass.com": "dsocolobsky", 415 "ignacio.avecilla@lambdaclass.com": "IAvecilla", 416 "duerzy@gmail.com": "duerzy", 417 "emozilla@nousresearch.com": "emozilla", 418 "fancydirty@gmail.com": "fancydirty", 419 "farion1231@gmail.com": "farion1231", 420 "floptopbot33@gmail.com": "flobo3", 421 "fontana.pedro93@gmail.com": "pefontana", 422 "francis.x.fitzpatrick@gmail.com": "fxfitz", 423 "frank@helmschrott.de": "Helmi", 424 "gaixg94@gmail.com": "gaixianggeng", 425 "geoff.wellman@gmail.com": "geoffwellman", 426 "han.shan@live.cn": "jamesarch", 427 "haolong@microsoft.com": "LongOddCode", 428 "hata1234@gmail.com": "hata1234", 429 "hmbown@gmail.com": "Hmbown", 430 "iacobs@m0n5t3r.info": "m0n5t3r", 431 "jiayuw794@gmail.com": "JiayuuWang", 432 "jonny@nousresearch.com": "jquesnelle", 433 "juan.ovalle@mistral.ai": "jjovalle99", 434 "julien.talbot@ergonomia.re": "Julientalbot", 435 "kagura.chen28@gmail.com": "kagura-agent", 436 "1342088860@qq.com": "youngDoo", 437 "kamil@gwozdz.me": "kamil-gwozdz", 438 "skmishra1991@gmail.com": "bugkill3r", 439 "karamusti912@gmail.com": "MustafaKara7", 440 "kira@ariaki.me": "kira-ariaki", 441 "knopki@duck.com": "knopki", 442 "limars874@gmail.com": "limars874", 443 "lisicheng168@gmail.com": "lesterli", 444 "mingjwan@microsoft.com": "MagicRay1217", 445 "orangeko@gmail.com": "GenKoKo", 446 "82095453+iacker@users.noreply.github.com": "iacker", 447 "sontianye@users.noreply.github.com": "sontianye", 448 "jackjin1997@users.noreply.github.com": "jackjin1997", 449 "1037461232@qq.com": "jackjin1997", 450 "danieldoderlein@users.noreply.github.com": "danieldoderlein", 451 "lrawnsley@users.noreply.github.com": "lrawnsley", 452 "taeuk178@users.noreply.github.com": "taeuk178", 453 "ogzerber@users.noreply.github.com": "ogzerber", 454 "cola-runner@users.noreply.github.com": "cola-runner", 455 "ygd58@users.noreply.github.com": "ygd58", 456 "45554392+warabe1122@users.noreply.github.com": "warabe1122", 457 "187001140+willy-scr@users.noreply.github.com": "willy-scr", 458 "vominh1919@users.noreply.github.com": "vominh1919", 459 "iamagenius00@users.noreply.github.com": "iamagenius00", 460 "9219265+cresslank@users.noreply.github.com": "cresslank", 461 "trevmanthony@gmail.com": "trevthefoolish", 462 "ziliangpeng@users.noreply.github.com": "ziliangpeng", 463 "centripetal-star@users.noreply.github.com": "centripetal-star", 464 "LeonSGP43@users.noreply.github.com": "LeonSGP43", 465 "154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43", 466 "cine.dreamer.one@gmail.com": "LeonSGP43", 467 "Lubrsy706@users.noreply.github.com": "Lubrsy706", 468 "niyant@spicefi.xyz": "spniyant", 469 "olafthiele@gmail.com": "olafthiele", 470 "oncuevtv@gmail.com": "sprmn24", 471 "programming@olafthiele.com": "olafthiele", 472 "r2668940489@gmail.com": "r266-tech", 473 "s5460703@gmail.com": "BlackishGreen33", 474 "saul.jj.wu@gmail.com": "SaulJWu", 475 "shenhaocheng19990111@gmail.com": "hcshen0111", 476 "sjtuwbh@gmail.com": "Cygra", 477 "srhtsrht17@gmail.com": "Sertug17", 478 "stephenschoettler@gmail.com": "stephenschoettler", 479 "tanishq231003@gmail.com": "yyovil", 480 "taosiyuan163@153.com": "taosiyuan163", 481 "tesseracttars@gmail.com": "tesseracttars-creator", 482 "tianliangjay@gmail.com": "xingkongliang", 483 "1317078257maroon@gmail.com": "Oxidane-bot", 484 "tranquil_flow@protonmail.com": "Tranquil-Flow", 485 "LyleLengyel@gmail.com": "mcndjxlefnd", 486 "unayung@gmail.com": "Unayung", 487 "vorvul.danylo@gmail.com": "WorldInnovationsDepartment", 488 "win4r@outlook.com": "win4r", 489 "xush@xush.org": "KUSH42", 490 "yangzhi.see@gmail.com": "SeeYangZhi", 491 "yongtenglei@gmail.com": "yongtenglei", 492 "young@YoungdeMacBook-Pro.local": "YoungYang963", 493 "ysfalweshcan@gmail.com": "Junass1", 494 "ysfwaxlycan@gmail.com": "WAXLYY", 495 "yusufalweshdemir@gmail.com": "Dusk1e", 496 "zhouboli@gmail.com": "zhouboli", 497 "zqiao@microsoft.com": "tomqiaozc", 498 "zzn+pa@zzn.im": "xinbenlv", 499 "zaynjarvis@gmail.com": "ZaynJarvis", 500 "zhiheng.liu@bytedance.com": "ZaynJarvis", 501 "izhaolongfei@gmail.com": "loongfay", 502 "296659110@qq.com": "lrt4836", 503 "fe.daniel91@gmail.com": "beforeload", 504 "libo1106@foxmail.com": "libo1106", 505 "295367131@qq.com": "295367131", 506 "295367132@qq.com": "IxAres", 507 "danieldliu@tencent.com": "danieldliu", 508 "loongzhao@tencent.com": "loongzhao", 509 "Bartok9@users.noreply.github.com": "Bartok9", 510 "LeonSGP43@users.noreply.github.com": "LeonSGP43", 511 "kshitijk4poor@users.noreply.github.com": "kshitijk4poor", 512 "mbelleau@Michels-MacBook-Pro.local": "malaiwah", 513 "michel.belleau@malaiwah.com": "malaiwah", 514 "gnanasekaran.sekareee@gmail.com": "gnanam1990", 515 "jz.pentest@gmail.com": "0xyg3n", 516 "7093928+0xyg3n@users.noreply.github.com": "0xyg3n", 517 "nftpoetrist@gmail.com": "nftpoetrist", # PR #18982 518 "millerc79@users.noreply.github.com": "millerc79", # PR #19033 519 "hermes@example.com": "shellybotmoyer", # PR #18915 (bot-committed) 520 "exx@example.com": "exxmen", # PR #19555 521 "hypnosis.mda@gmail.com": "Hypn0sis", 522 "ywt000818@gmail.com": "OwenYWT", 523 "dhandhalyabhavik@gmail.com": "v1k22", 524 "rucchizhao@zhaochenfeideMacBook-Pro.local": "RucchiZ", 525 "tannerfokkens@Mac.attlocal.net": "tannerfokkens-maker", 526 "lehaolin98@outlook.com": "LehaoLin", 527 "yuewang1@microsoft.com": "imink", 528 "1736355688@qq.com": "hedgeho9X", 529 "bernylinville@devopsthink.org": "bernylinville", 530 "brian@bde.io": "briandevans", 531 "hubin_ll@qq.com": "LLQWQ", 532 "memosr_email@gmail.com": "memosr", 533 "jperlow@gmail.com": "perlowja", 534 "jasonpette1783@gmail.com": "web-dev0521", 535 "tangyuanjc@JCdeAIfenshendeMac-mini.local": "tangyuanjc", 536 "harryplusplus@gmail.com": "harryplusplus", 537 "anthhub@163.com": "anthhub", 538 "allard.quek@singtel.com": "AllardQuek", 539 "shenuu@gmail.com": "shenuu", 540 "xiayh17@gmail.com": "xiayh0107", 541 "zhujianxyz@gmail.com": "opriz", 542 "asurla@nvidia.com": "anniesurla", 543 "limkuan24@gmail.com": "WideLee", 544 "aviralarora002@gmail.com": "AviArora02-commits", 545 "draixagent@gmail.com": "draix", 546 "junminliu@gmail.com": "JimLiu", 547 "jarvischer@gmail.com": "maxchernin", 548 "levantam.98.2324@gmail.com": "LVT382009", 549 "zhurongcheng@rcrai.com": "heykb", 550 "withapurpose37@gmail.com": "StefanIsMe", 551 "261797239+lumenradley@users.noreply.github.com": "lumenradley", 552 "166376523+sjz-ks@users.noreply.github.com": "sjz-ks", 553 "haileymarshall005@gmail.com": "haileymarshall", 554 "aniruddhaadak80@users.noreply.github.com": "aniruddhaadak80", 555 "zheng.jerilyn@gmail.com": "jerilynzheng", 556 "asslaenn5@gmail.com": "Aslaaen", 557 "shalompmc0505@naver.com": "pinion05", 558 "105142614+VTRiot@users.noreply.github.com": "VTRiot", 559 "vivien000812@gmail.com": "iamagenius00", 560 "89228157+Feranmi10@users.noreply.github.com": "Feranmi10", 561 "oluwadareferanmi11@gmail.com": "Feranmi10", 562 "simon@gtcl.us": "simon-gtcl", 563 "suzukaze.haduki@gmail.com": "houko", 564 "cliff@cigii.com": "cgarwood82", 565 "anna@oa.ke": "anna-oake", 566 "jaffarkeikei@gmail.com": "jaffarkeikei", 567 "hxp@hxp.plus": "hxp-plus", 568 "3580442280@qq.com": "Tianworld", 569 "wujianxu91@gmail.com": "wujhsu", 570 "zhrh120@gmail.com": "niyoh120", 571 "vrinek@hey.com": "vrinek", 572 "268198004+xandersbell@users.noreply.github.com": "xandersbell", 573 "somme4096@gmail.com": "Somme4096", 574 "brian@tiuxo.com": "brianclemens", 575 "25944632+yudaiyan@users.noreply.github.com": "yudaiyan", 576 "chayton@sina.com": "ycbai", 577 "longsizhuo@gmail.com": "longsizhuo", 578 "chenb19870707@gmail.com": "ms-alan", 579 "276886827+WuTianyi123@users.noreply.github.com": "WuTianyi123", 580 "22549957+li0near@users.noreply.github.com": "li0near", 581 "23434080+sicnuyudidi@users.noreply.github.com": "sicnuyudidi", 582 "haimu0x0@proton.me": "haimu0x", 583 "abdelmajidnidnasser1@gmail.com": "NIDNASSER-Abdelmajid", 584 "projectadmin@wit.id": "projectadmin-dev", 585 "mrigankamondal10@gmail.com": "Dev-Mriganka", 586 "132275809+shushuzn@users.noreply.github.com": "shushuzn", 587 "ibrahimozsarac@gmail.com": "iborazzi", 588 "130149563+A-afflatus@users.noreply.github.com": "A-afflatus", 589 "huangkwell@163.com": "huangke19", 590 "tanishq@exa.ai": "10ishq", 591 "363708+christopherwoodall@users.noreply.github.com": "christopherwoodall", 592 "zhang9w0v5@qq.com": "zhang9w0v5", 593 "fuleinist@outlook.com": "fuleinist", 594 "43494187+Llugaes@users.noreply.github.com": "Llugaes", 595 "fengtianyu88@users.noreply.github.com": "fengtianyu88", 596 "l.moncany@gmail.com": "lmoncany", 597 "fatinghenji@users.noreply.github.com": "fatinghenji", 598 "xin.peng.dr@gmail.com": "xinpengdr", 599 "mike@mikewaters.net": "mikewaters", 600 "65117428+WadydX@users.noreply.github.com": "WadydX", 601 "216480837+isaachuangGMICLOUD@users.noreply.github.com": "isaachuangGMICLOUD", 602 "nukuom976228@gmail.com": "hsy5571616", 603 "11462216+Nan93@users.noreply.github.com": "Nan93", 604 "l973401489@126.com": "zhouxiaoya12", 605 "373119611@qq.com": "roytian1217", 606 "brett@brettbrewer.com": "minorgod", 607 "67779267+wenhao7@users.noreply.github.com": "wenhao7", 608 "git@yzx9.xyz": "yzx9", 609 "nilesh@cloudgeni.us": "lvnilesh", 610 "63502660+azhengbot@users.noreply.github.com": "azhengbot", 611 "sharvil.saxena@gmail.com": "sharziki", 612 "yuanhe@minimaxi.com": "RyanLee-Dev", 613 "curtis992250@gmail.com": "TaroballzChen", 614 "92638503+Lind3ey@users.noreply.github.com": "Lind3ey", 615 "1352808998@qq.com": "phpoh", 616 "caliberoviv@gmail.com": "vivganes", 617 "michaelfackerell@gmail.com": "MikeFac", 618 "18024642@qq.com": "GuyCui", 619 "eumael.mkt@gmail.com": "maelrx", 620 # v0.11.0 additions 621 "benbarclay@gmail.com": "benbarclay", 622 "lijiawen@umich.edu": "Jiawen-lee", 623 "oleksiy@kovyrin.net": "kovyrin", 624 "kovyrin.claw@gmail.com": "kovyrin", 625 "kaiobarb@gmail.com": "liftaris", 626 "me@arihantsethia.com": "arihantsethia", 627 "zhuofengwang2003@gmail.com": "coekfung", 628 "teknium@noreply.github.com": "teknium1", 629 "2114364329@qq.com": "cuyua9", 630 "2557058999@qq.com": "Disaster-Terminator", 631 "cine.dreamer.one@gmail.com": "LeonSGP43", 632 "zyprothh@gmail.com": "Zyproth", 633 "amitgaur@gmail.com": "amitgaur", 634 "albuquerque.abner@gmail.com": "mrbob-git", 635 "kiala@users.noreply.github.com": "kiala9", 636 "alanxchen@gmail.com": "alanxchen85", 637 "clawbot@clawbots-Mac-mini.local": "John-tip", 638 "der@konsi.org": "konsisumer", 639 "cirwel@The-CIRWEL-Group.local": "CIRWEL", 640 "molvikar8@gmail.com": "molvikar", 641 "nftpoetrist@gmail.com": "nftpoetrist", 642 "dodofun@126.com": "colorcross", 643 "1615063567@qq.com": "zhao0112", 644 "ethanguo.2003@gmail.com": "EthanGuo-coder", 645 "dev0jsh@gmail.com": "tmdgusya", 646 "leavr@163.com": "leavrcn", 647 "17683456+wanazhar@users.noreply.github.com": "wanazhar", 648 "26782336+cixuuz@users.noreply.github.com": "cixuuz", 649 "aleksandr.pasevin@openzeppelin.com": "pasevin", 650 "ubuntu@localhost.localdomain": "holynn-q", 651 "holynn@placeholder.local": "holynn-q", 652 "agent@hermes.local": "jacdevos", 653 "sunsky.lau@gmail.com": "liuhao1024", 654 "qiuqfang98@qq.com": "keepcalmqqf", 655 "261867348+ai-ag2026@users.noreply.github.com": "ai-ag2026", 656 "yanzh.su@gmail.com": "YanzhongSu", 657 "wanderwang@users.noreply.github.com": "WanderWang", 658 "yueheime@gmail.com": "yuehei", 659 "emidomh@gmail.com": "Emidomenge", 660 "2642448440@qq.com": "BlackJulySnow", 661 "4317663+helix4u@users.noreply.github.com": "helix4u", 662 "floptopbot33@gmail.com": "flobo3", 663 "dpaluy@users.noreply.github.com": "dpaluy", 664 "psikonetik@gmail.com": "el-analista", 665 "chenb19870707@gmail.com": "ms-alan", 666 "hex-clawd@users.noreply.github.com": "hex-clawd", 667 "154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43", 668 "barteq@hacknotes.local": "barteqpl", 669 "pama0227@gmail.com": "pama0227", 670 "52785845+ee-blog@users.noreply.github.com": "ee-blog", 671 "simplenamebox@gmail.com": "simplenamebox-ops", 672 "balyan.sid@gmail.com": "alt-glitch", 673 "xdord@xdorddeMac-mini.local": "foreverxdord", 674 "k2767567815@gmail.com": "QifengKuang", 675 "88077783+jjjojoj@users.noreply.github.com": "jjjojoj", 676 "valda@underscore.jp": "valda", 677 "lling486@163.com": "M3RCUR2Y", 678 "buraysandro9@gmail.com": "ygd58", 679 "ideathinklab01-source@users.noreply.github.com": "ideathinklab01-source", 680 "27987889@qq.com": "zng8418", 681 "daniuxie88@proton.me": "DaniuXie", 682 "panchanler@gmail.com": "ChanlerDev", 683 "252620095+briandevans@users.noreply.github.com": "briandevans", 684 "141889580+h0tp-ftw@users.noreply.github.com": "h0tp-ftw", 685 "chinadbo@foxmail.com": "chinadbo", 686 "82637225+kshitijk4poor@users.noreply.github.com": "kshitijk4poor", 687 "xyywtt@gmail.com": "xyiy001", 688 "charliekerfoot@gmail.com": "CharlieKerfoot", 689 "grey0202@users.noreply.github.com": "Grey0202", 690 "vominh1919@gmail.com": "vominh1919", 691 "giwavictor9@gmail.com": "giwaov", 692 "yoimexex@gmail.com": "Yoimex", 693 "76803960+atongrun@users.noreply.github.com": "atongrun", 694 "michaeldanko@icloud.com": "MichaelWDanko", 695 "xudavid429@gmail.com": "YX234", 696 "kathy@Kathy.local": "julysir", 697 "274902531@qq.com": "JanCong", 698 "225304168+e-shizz@users.noreply.github.com": "e-shizz", 699 "vincent_hh@users.noreply.github.com": "VinVC", 700 "1243352777@qq.com": "zons-zhaozhy", 701 "dejie.guo@gmail.com": "JayGwod", 702 "52840391+swithek@users.noreply.github.com": "swithek", 703 "raipratik0101@gmail.com": "PratikRai0101", 704 "code@sasha.id": "sasha-id", 705 "chen.yunbo@xydigit.com": "chenyunbo411", 706 "openclaw@local": "Asce66", 707 "59465365+0xsir0000@users.noreply.github.com": "0xsir0000", 708 "lisanhu2014@hotmail.com": "lisanhu", 709 "0668001438@zte.com.cn": "chenyunbo411", 710 "leozeli@qq.com": "leozeli", 711 "linlehao@cuhk.edu.cn": "LehaoLin", 712 "liutong@isacas.ac.cn": "I3eg1nner", 713 "peterberthelsen@Peters-MacBook-Air.local": "PeterBerthelsen", 714 "root@debian.debian": "lengxii", 715 "roque@priveperfumeshn.com": "priveperfumes", 716 "shijianzhi@shijianzhideMacBook-Pro.local": "sjz-ks", 717 "topcheer@me.com": "topcheer", 718 "walli@tencent.com": "walli", 719 "zhuofengwang@tencent.com": "Zhuofeng-Wang", 720 "simonweng@tencent.com": "Contentment003111", 721 # April 2026 salvage-PR batch (#14920, #14986, #14966) 722 "mrunmayeerane17@gmail.com": "mrunmayee17", 723 "69489633+camaragon@users.noreply.github.com": "camaragon", 724 "shamork@outlook.com": "shamork", 725 # April 2026 Discord Copilot /model salvage (#15030) 726 "cshong2017@outlook.com": "Nicecsh", 727 # no-github-match — keep as display names 728 "clio-agent@sisyphuslabs.ai": "Sisyphus", 729 "marco@rutimka.de": "Marco Rutsch", 730 "paul@gamma.app": "Paul Bergeron", 731 "zhangxicen@example.com": "zhangxicen", 732 "codex@openai.invalid": "teknium1", 733 "screenmachine@gmail.com": "teknium1", 734 "chenzeshi@live.com": "chen1749144759", 735 "mor.aleksandr@yahoo.com": "MorAlekss", 736 "276649498+ztexydt-cqh@users.noreply.github.com": "ztexydt-cqh", 737 "ash@users.noreply.github.com": "ash", 738 "andrewho.sf@gmail.com": "andrewhosf", 739 # April 2026 Honcho bug-fix consolidation (#15381) 740 "HiddenPuppy@users.noreply.github.com": "HiddenPuppy", 741 "code@sasha.id": "sasha-id", 742 "dontcallmejames@users.noreply.github.com": "dontcallmejames", 743 "hekaru.agent@gmail.com": "hekaru-agent", 744 "jas9000@gmail.com": "twozle", 745 "r.filgueiras@apheris.com": "rfilgueiras", 746 "leihaibo1992@gmail.com": "Leihb", 747 # ACP streaming fix salvage (PR #9428 + #16273) 748 "nfb0408@163.com": "ningfangbin", 749 "164839249+Joseph19820124@users.noreply.github.com": "Joseph19820124", 750 "rugved@lmstudio.ai": "rugvedS07", 751 "44333070+Heltman@users.noreply.github.com": "Heltman", 752 # v0.12.0 additions 753 "ching@kachingappz.com": "ching-kaching", 754 "codezhujr@gmail.com": "Zjianru", # salvage chain: code by codez, PR #15749 author @Zjianru 755 "daimon@noreply.github.com": "Siddharth Balyan", # co-author only 756 "i@zkl2333.com": "zkl2333", 757 "isaachuang@Isaacs-MacBook-Pro.local": "isaachuangGMICLOUD", 758 "isaachuang@Mac.localdomain": "isaachuangGMICLOUD", # salvage of PR #11955 → #16663 759 "liyuan851277048@icloud.com": "Octopus", # co-author only 760 "me+github7604@versun.org": "Versun", # co-author only 761 "my.vesper.nine@gmail.com": "kevin-ho", # salvage: PR #15488 author @kevin-ho 762 "noreply@paperclip.ing": "Paperclip", # co-author only 763 "teknium@hermes-agent": "teknium1", 764 "web3blind@gmail.com": "web3blind", 765 "ztzheng@163.com": "chengoak", # PR #17467 766 "24110240104@m.fudan.edu.cn": "YuShu", # co-author only 767 "charliekerfoot@gmail.com": "CharlieKerfoot", # PR #18951 768 # Debug share upload-time redaction (May 2026) 769 "dhuysamen@gmail.com": "GodsBoy", # PR #19318 770 } 771 772 773 def git(*args, cwd=None): 774 """Run a git command and return stdout.""" 775 result = subprocess.run( 776 ["git"] + list(args), 777 capture_output=True, text=True, 778 cwd=cwd or str(REPO_ROOT), 779 ) 780 if result.returncode != 0: 781 print(f"git {' '.join(args)} failed: {result.stderr}", file=sys.stderr) 782 return "" 783 return result.stdout.strip() 784 785 786 def git_result(*args, cwd=None): 787 """Run a git command and return the full CompletedProcess.""" 788 return subprocess.run( 789 ["git"] + list(args), 790 capture_output=True, 791 text=True, 792 cwd=cwd or str(REPO_ROOT), 793 ) 794 795 796 def get_last_tag(): 797 """Get the most recent CalVer tag.""" 798 tags = git("tag", "--list", "v20*", "--sort=-v:refname") 799 if tags: 800 return tags.split("\n")[0] 801 return None 802 803 804 def next_available_tag(base_tag: str) -> tuple[str, str]: 805 """Return a tag/calver pair, suffixing same-day releases when needed.""" 806 if not git("tag", "--list", base_tag): 807 return base_tag, base_tag.removeprefix("v") 808 809 suffix = 2 810 while git("tag", "--list", f"{base_tag}.{suffix}"): 811 suffix += 1 812 tag_name = f"{base_tag}.{suffix}" 813 return tag_name, tag_name.removeprefix("v") 814 815 816 def get_current_version(): 817 """Read current semver from __init__.py.""" 818 content = VERSION_FILE.read_text() 819 match = re.search(r'__version__\s*=\s*"([^"]+)"', content) 820 return match.group(1) if match else "0.0.0" 821 822 823 def bump_version(current: str, part: str) -> str: 824 """Bump a semver version string.""" 825 parts = current.split(".") 826 if len(parts) != 3: 827 parts = ["0", "0", "0"] 828 major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2]) 829 830 if part == "major": 831 major += 1 832 minor = 0 833 patch = 0 834 elif part == "minor": 835 minor += 1 836 patch = 0 837 elif part == "patch": 838 patch += 1 839 else: 840 raise ValueError(f"Unknown bump part: {part}") 841 842 return f"{major}.{minor}.{patch}" 843 844 845 def update_version_files(semver: str, calver_date: str): 846 """Update version strings in source files.""" 847 # Update __init__.py 848 content = VERSION_FILE.read_text() 849 content = re.sub( 850 r'__version__\s*=\s*"[^"]+"', 851 f'__version__ = "{semver}"', 852 content, 853 ) 854 content = re.sub( 855 r'__release_date__\s*=\s*"[^"]+"', 856 f'__release_date__ = "{calver_date}"', 857 content, 858 ) 859 VERSION_FILE.write_text(content) 860 861 # Update pyproject.toml 862 pyproject = PYPROJECT_FILE.read_text() 863 pyproject = re.sub( 864 r'^version\s*=\s*"[^"]+"', 865 f'version = "{semver}"', 866 pyproject, 867 flags=re.MULTILINE, 868 ) 869 PYPROJECT_FILE.write_text(pyproject) 870 871 872 def build_release_artifacts(semver: str) -> list[Path]: 873 """Build sdist/wheel artifacts for the current release. 874 875 Returns the artifact paths when the local environment has ``python -m build`` 876 available. If build tooling is missing or the build fails, returns an empty 877 list and lets the release proceed without attached Python artifacts. 878 """ 879 dist_dir = REPO_ROOT / "dist" 880 shutil.rmtree(dist_dir, ignore_errors=True) 881 882 result = subprocess.run( 883 [sys.executable, "-m", "build", "--sdist", "--wheel"], 884 cwd=str(REPO_ROOT), 885 capture_output=True, 886 text=True, 887 ) 888 if result.returncode != 0: 889 print(" ⚠ Could not build Python release artifacts.") 890 stderr = result.stderr.strip() 891 stdout = result.stdout.strip() 892 if stderr: 893 print(f" {stderr.splitlines()[-1]}") 894 elif stdout: 895 print(f" {stdout.splitlines()[-1]}") 896 print(" Install the 'build' package to attach semver-named sdist/wheel assets.") 897 return [] 898 899 artifacts = sorted(p for p in dist_dir.iterdir() if p.is_file()) 900 matching = [p for p in artifacts if semver in p.name] 901 if not matching: 902 print(" ⚠ Built artifacts did not match the expected release version.") 903 return [] 904 return matching 905 906 907 def resolve_author(name: str, email: str) -> str: 908 """Resolve a git author to a GitHub @mention.""" 909 # Try email lookup first 910 gh_user = AUTHOR_MAP.get(email) 911 if gh_user: 912 return f"@{gh_user}" 913 914 # Try noreply pattern 915 noreply_match = re.match(r"(\d+)\+(.+)@users\.noreply\.github\.com", email) 916 if noreply_match: 917 return f"@{noreply_match.group(2)}" 918 919 # Try username@users.noreply.github.com 920 noreply_match2 = re.match(r"(.+)@users\.noreply\.github\.com", email) 921 if noreply_match2: 922 return f"@{noreply_match2.group(1)}" 923 924 # Fallback to git name 925 return name 926 927 928 def categorize_commit(subject: str) -> str: 929 """Categorize a commit by its conventional commit prefix.""" 930 subject_lower = subject.lower() 931 932 # Match conventional commit patterns 933 patterns = { 934 "breaking": [r"^breaking[\s:(]", r"^!:", r"BREAKING CHANGE"], 935 "features": [r"^feat[\s:(]", r"^feature[\s:(]", r"^add[\s:(]"], 936 "fixes": [r"^fix[\s:(]", r"^bugfix[\s:(]", r"^bug[\s:(]", r"^hotfix[\s:(]"], 937 "improvements": [r"^improve[\s:(]", r"^perf[\s:(]", r"^enhance[\s:(]", 938 r"^refactor[\s:(]", r"^cleanup[\s:(]", r"^clean[\s:(]", 939 r"^update[\s:(]", r"^optimize[\s:(]"], 940 "docs": [r"^doc[\s:(]", r"^docs[\s:(]"], 941 "tests": [r"^test[\s:(]", r"^tests[\s:(]"], 942 "chore": [r"^chore[\s:(]", r"^ci[\s:(]", r"^build[\s:(]", 943 r"^deps[\s:(]", r"^bump[\s:(]"], 944 } 945 946 for category, regexes in patterns.items(): 947 for regex in regexes: 948 if re.match(regex, subject_lower): 949 return category 950 951 # Heuristic fallbacks 952 if any(w in subject_lower for w in ["add ", "new ", "implement", "support "]): 953 return "features" 954 if any(w in subject_lower for w in ["fix ", "fixed ", "resolve", "patch "]): 955 return "fixes" 956 if any(w in subject_lower for w in ["refactor", "cleanup", "improve", "update "]): 957 return "improvements" 958 959 return "other" 960 961 962 def clean_subject(subject: str) -> str: 963 """Clean up a commit subject for display.""" 964 # Remove conventional commit prefix 965 cleaned = re.sub(r"^(feat|fix|docs|chore|refactor|test|perf|ci|build|improve|add|update|cleanup|hotfix|breaking|enhance|optimize|bugfix|bug|feature|tests|deps|bump)[\s:(!]+\s*", "", subject, flags=re.IGNORECASE) 966 # Remove trailing issue refs that are redundant with PR links 967 cleaned = cleaned.strip() 968 # Capitalize first letter 969 if cleaned: 970 cleaned = cleaned[0].upper() + cleaned[1:] 971 return cleaned 972 973 974 def parse_coauthors(body: str) -> list: 975 """Extract Co-authored-by trailers from a commit message body. 976 977 Returns a list of {'name': ..., 'email': ...} dicts. 978 Filters out AI assistants and bots (Claude, Copilot, Cursor, etc.). 979 """ 980 if not body: 981 return [] 982 # AI/bot emails to ignore in co-author trailers 983 _ignored_emails = {"noreply@anthropic.com", "noreply@github.com", 984 "cursoragent@cursor.com", "hermes@nousresearch.com"} 985 _ignored_names = re.compile(r"^(Claude|Copilot|Cursor Agent|GitHub Actions?|dependabot|renovate)", re.IGNORECASE) 986 pattern = re.compile(r"Co-authored-by:\s*(.+?)\s*<([^>]+)>", re.IGNORECASE) 987 results = [] 988 for m in pattern.finditer(body): 989 name, email = m.group(1).strip(), m.group(2).strip() 990 if email in _ignored_emails or _ignored_names.match(name): 991 continue 992 results.append({"name": name, "email": email}) 993 return results 994 995 996 def get_commits(since_tag=None): 997 """Get commits since a tag (or all commits if None).""" 998 if since_tag: 999 range_spec = f"{since_tag}..HEAD" 1000 else: 1001 range_spec = "HEAD" 1002 1003 # Format: hash|author_name|author_email|subject\0body 1004 # Using %x00 (null) as separator between subject and body 1005 log = git( 1006 "log", range_spec, 1007 "--format=%H|%an|%ae|%s%x00%b%x00", 1008 "--no-merges", 1009 ) 1010 1011 if not log: 1012 return [] 1013 1014 commits = [] 1015 # Split on double-null to get each commit entry, since body ends with \0 1016 # and format ends with \0, each record ends with \0\0 between entries 1017 for entry in log.split("\0\0"): 1018 entry = entry.strip() 1019 if not entry: 1020 continue 1021 # Split on first null to separate "hash|name|email|subject" from "body" 1022 if "\0" in entry: 1023 header, body = entry.split("\0", 1) 1024 body = body.strip() 1025 else: 1026 header = entry 1027 body = "" 1028 parts = header.split("|", 3) 1029 if len(parts) != 4: 1030 continue 1031 sha, name, email, subject = parts 1032 coauthor_info = parse_coauthors(body) 1033 coauthors = [resolve_author(ca["name"], ca["email"]) for ca in coauthor_info] 1034 commits.append({ 1035 "sha": sha, 1036 "short_sha": sha[:8], 1037 "author_name": name, 1038 "author_email": email, 1039 "subject": subject, 1040 "category": categorize_commit(subject), 1041 "github_author": resolve_author(name, email), 1042 "coauthors": coauthors, 1043 }) 1044 1045 return commits 1046 1047 1048 def get_pr_number(subject: str) -> str: 1049 """Extract PR number from commit subject if present.""" 1050 match = re.search(r"#(\d+)", subject) 1051 if match: 1052 return match.group(1) 1053 return None 1054 1055 1056 def generate_changelog(commits, tag_name, semver, repo_url="https://github.com/NousResearch/hermes-agent", 1057 prev_tag=None, first_release=False): 1058 """Generate markdown changelog from categorized commits.""" 1059 lines = [] 1060 1061 # Header 1062 now = datetime.now() 1063 date_str = now.strftime("%B %d, %Y") 1064 lines.append(f"# Hermes Agent v{semver} ({tag_name})") 1065 lines.append("") 1066 lines.append(f"**Release Date:** {date_str}") 1067 lines.append("") 1068 1069 if first_release: 1070 lines.append("> 🎉 **First official release!** This marks the beginning of regular weekly releases") 1071 lines.append("> for Hermes Agent. See below for everything included in this initial release.") 1072 lines.append("") 1073 1074 # Group commits by category 1075 categories = defaultdict(list) 1076 all_authors = set() 1077 teknium_aliases = {"@teknium1"} 1078 1079 for commit in commits: 1080 categories[commit["category"]].append(commit) 1081 author = commit["github_author"] 1082 if author not in teknium_aliases: 1083 all_authors.add(author) 1084 for coauthor in commit.get("coauthors", []): 1085 if coauthor not in teknium_aliases: 1086 all_authors.add(coauthor) 1087 1088 # Category display order and emoji 1089 category_order = [ 1090 ("breaking", "⚠️ Breaking Changes"), 1091 ("features", "✨ Features"), 1092 ("improvements", "🔧 Improvements"), 1093 ("fixes", "🐛 Bug Fixes"), 1094 ("docs", "📚 Documentation"), 1095 ("tests", "🧪 Tests"), 1096 ("chore", "🏗️ Infrastructure"), 1097 ("other", "📦 Other Changes"), 1098 ] 1099 1100 for cat_key, cat_title in category_order: 1101 cat_commits = categories.get(cat_key, []) 1102 if not cat_commits: 1103 continue 1104 1105 lines.append(f"## {cat_title}") 1106 lines.append("") 1107 1108 for commit in cat_commits: 1109 subject = clean_subject(commit["subject"]) 1110 pr_num = get_pr_number(commit["subject"]) 1111 author = commit["github_author"] 1112 1113 # Build the line 1114 parts = [f"- {subject}"] 1115 if pr_num: 1116 parts.append(f"([#{pr_num}]({repo_url}/pull/{pr_num}))") 1117 else: 1118 parts.append(f"([`{commit['short_sha']}`]({repo_url}/commit/{commit['sha']}))") 1119 1120 if author not in teknium_aliases: 1121 parts.append(f"— {author}") 1122 1123 lines.append(" ".join(parts)) 1124 1125 lines.append("") 1126 1127 # Contributors section 1128 if all_authors: 1129 # Sort contributors by commit count 1130 author_counts = defaultdict(int) 1131 for commit in commits: 1132 author = commit["github_author"] 1133 if author not in teknium_aliases: 1134 author_counts[author] += 1 1135 for coauthor in commit.get("coauthors", []): 1136 if coauthor not in teknium_aliases: 1137 author_counts[coauthor] += 1 1138 1139 sorted_authors = sorted(author_counts.items(), key=lambda x: -x[1]) 1140 1141 lines.append("## 👥 Contributors") 1142 lines.append("") 1143 lines.append("Thank you to everyone who contributed to this release!") 1144 lines.append("") 1145 for author, count in sorted_authors: 1146 commit_word = "commit" if count == 1 else "commits" 1147 lines.append(f"- {author} ({count} {commit_word})") 1148 lines.append("") 1149 1150 # Full changelog link 1151 if prev_tag: 1152 lines.append(f"**Full Changelog**: [{prev_tag}...{tag_name}]({repo_url}/compare/{prev_tag}...{tag_name})") 1153 else: 1154 lines.append(f"**Full Changelog**: [{tag_name}]({repo_url}/commits/{tag_name})") 1155 lines.append("") 1156 1157 return "\n".join(lines) 1158 1159 1160 def main(): 1161 parser = argparse.ArgumentParser(description="Hermes Agent Release Tool") 1162 parser.add_argument("--bump", choices=["major", "minor", "patch"], 1163 help="Which semver component to bump") 1164 parser.add_argument("--publish", action="store_true", 1165 help="Actually create the tag and GitHub release (otherwise dry run)") 1166 parser.add_argument("--date", type=str, 1167 help="Override CalVer date (format: YYYY.M.D)") 1168 parser.add_argument("--first-release", action="store_true", 1169 help="Mark as first release (no previous tag expected)") 1170 parser.add_argument("--output", type=str, 1171 help="Write changelog to file instead of stdout") 1172 args = parser.parse_args() 1173 1174 # Determine CalVer date 1175 if args.date: 1176 calver_date = args.date 1177 else: 1178 now = datetime.now() 1179 calver_date = f"{now.year}.{now.month}.{now.day}" 1180 1181 base_tag = f"v{calver_date}" 1182 tag_name, calver_date = next_available_tag(base_tag) 1183 if tag_name != base_tag: 1184 print(f"Note: Tag {base_tag} already exists, using {tag_name}") 1185 1186 # Determine semver 1187 current_version = get_current_version() 1188 if args.bump: 1189 new_version = bump_version(current_version, args.bump) 1190 else: 1191 new_version = current_version 1192 1193 # Get previous tag 1194 prev_tag = get_last_tag() 1195 if not prev_tag and not args.first_release: 1196 print("No previous tags found. Use --first-release for the initial release.") 1197 print(f"Would create tag: {tag_name}") 1198 print(f"Would set version: {new_version}") 1199 1200 # Get commits 1201 commits = get_commits(since_tag=prev_tag) 1202 if not commits: 1203 print("No new commits since last tag.") 1204 if not args.first_release: 1205 return 1206 1207 print(f"{'='*60}") 1208 print(f" Hermes Agent Release Preview") 1209 print(f"{'='*60}") 1210 print(f" CalVer tag: {tag_name}") 1211 print(f" SemVer: v{current_version} → v{new_version}") 1212 print(f" Previous tag: {prev_tag or '(none — first release)'}") 1213 print(f" Commits: {len(commits)}") 1214 print(f" Unique authors: {len(set(c['github_author'] for c in commits))}") 1215 print(f" Mode: {'PUBLISH' if args.publish else 'DRY RUN'}") 1216 print(f"{'='*60}") 1217 print() 1218 1219 # Generate changelog 1220 changelog = generate_changelog( 1221 commits, tag_name, new_version, 1222 prev_tag=prev_tag, 1223 first_release=args.first_release, 1224 ) 1225 1226 if args.output: 1227 Path(args.output).write_text(changelog) 1228 print(f"Changelog written to {args.output}") 1229 else: 1230 print(changelog) 1231 1232 if args.publish: 1233 print(f"\n{'='*60}") 1234 print(" Publishing release...") 1235 print(f"{'='*60}") 1236 1237 # Update version files 1238 if args.bump: 1239 update_version_files(new_version, calver_date) 1240 print(f" ✓ Updated version files to v{new_version} ({calver_date})") 1241 1242 # Commit version bump 1243 add_result = git_result("add", str(VERSION_FILE), str(PYPROJECT_FILE)) 1244 if add_result.returncode != 0: 1245 print(f" ✗ Failed to stage version files: {add_result.stderr.strip()}") 1246 return 1247 1248 commit_result = git_result( 1249 "commit", "-m", f"chore: bump version to v{new_version} ({calver_date})" 1250 ) 1251 if commit_result.returncode != 0: 1252 print(f" ✗ Failed to commit version bump: {commit_result.stderr.strip()}") 1253 return 1254 print(f" ✓ Committed version bump") 1255 1256 # Create annotated tag 1257 tag_result = git_result( 1258 "tag", "-a", tag_name, "-m", 1259 f"Hermes Agent v{new_version} ({calver_date})\n\nWeekly release" 1260 ) 1261 if tag_result.returncode != 0: 1262 print(f" ✗ Failed to create tag {tag_name}: {tag_result.stderr.strip()}") 1263 return 1264 print(f" ✓ Created tag {tag_name}") 1265 1266 # Push 1267 push_result = git_result("push", "origin", "HEAD", "--tags") 1268 if push_result.returncode == 0: 1269 print(f" ✓ Pushed to origin") 1270 else: 1271 print(f" ✗ Failed to push to origin: {push_result.stderr.strip()}") 1272 print(" Continue manually after fixing access:") 1273 print(" git push origin HEAD --tags") 1274 1275 # Build semver-named Python artifacts so downstream packagers 1276 # (e.g. Homebrew) can target them without relying on CalVer tag names. 1277 artifacts = build_release_artifacts(new_version) 1278 if artifacts: 1279 print(" ✓ Built release artifacts:") 1280 for artifact in artifacts: 1281 print(f" - {artifact.relative_to(REPO_ROOT)}") 1282 1283 # Create GitHub release 1284 changelog_file = REPO_ROOT / ".release_notes.md" 1285 changelog_file.write_text(changelog) 1286 1287 gh_cmd = [ 1288 "gh", "release", "create", tag_name, 1289 "--title", f"Hermes Agent v{new_version} ({calver_date})", 1290 "--notes-file", str(changelog_file), 1291 ] 1292 gh_cmd.extend(str(path) for path in artifacts) 1293 1294 gh_bin = shutil.which("gh") 1295 if gh_bin: 1296 result = subprocess.run( 1297 gh_cmd, 1298 capture_output=True, text=True, 1299 cwd=str(REPO_ROOT), 1300 ) 1301 else: 1302 result = None 1303 1304 if result and result.returncode == 0: 1305 changelog_file.unlink(missing_ok=True) 1306 print(f" ✓ GitHub release created: {result.stdout.strip()}") 1307 print(f"\n 🎉 Release v{new_version} ({tag_name}) published!") 1308 else: 1309 if result is None: 1310 print(" ✗ GitHub release skipped: `gh` CLI not found.") 1311 else: 1312 print(f" ✗ GitHub release failed: {result.stderr.strip()}") 1313 print(f" Release notes kept at: {changelog_file}") 1314 print(f" Tag was created locally. Create the release manually:") 1315 print( 1316 f" gh release create {tag_name} --title 'Hermes Agent v{new_version} ({calver_date})' " 1317 f"--notes-file .release_notes.md {' '.join(str(path) for path in artifacts)}" 1318 ) 1319 print(f"\n ✓ Release artifacts prepared for manual publish: v{new_version} ({tag_name})") 1320 else: 1321 print(f"\n{'='*60}") 1322 print(f" Dry run complete. To publish, add --publish") 1323 print(f" Example: python scripts/release.py --bump minor --publish") 1324 print(f"{'='*60}") 1325 1326 1327 if __name__ == "__main__": 1328 main()