| |
| """Hermes Agent Release Script |
| |
| Generates changelogs and creates GitHub releases with CalVer tags. |
| |
| Usage: |
| # Preview changelog (dry run) |
| python scripts/release.py |
| |
| # Preview with semver bump |
| python scripts/release.py --bump minor |
| |
| # Create the release |
| python scripts/release.py --bump minor --publish |
| |
| # First release (no previous tag) |
| python scripts/release.py --bump minor --publish --first-release |
| |
| # Override CalVer date (e.g. for a belated release) |
| python scripts/release.py --bump minor --publish --date 2026.3.15 |
| """ |
|
|
| import argparse |
| import re |
| import shutil |
| import subprocess |
| import sys |
| from collections import defaultdict |
| from datetime import datetime |
| from pathlib import Path |
|
|
| REPO_ROOT = Path(__file__).resolve().parent.parent |
| VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py" |
| PYPROJECT_FILE = REPO_ROOT / "pyproject.toml" |
|
|
| |
| |
| |
|
|
| |
| AUTHOR_MAP = { |
| |
| "teknium1@gmail.com": "teknium1", |
| "teknium@nousresearch.com": "teknium1", |
| "127238744+teknium1@users.noreply.github.com": "teknium1", |
| "343873859@qq.com": "DrStrangerUJN", |
| "jefferson@heimdallstrategy.com": "Mind-Dragon", |
| "130918800+devorun@users.noreply.github.com": "devorun", |
| "maks.mir@yahoo.com": "say8hi", |
| |
| "david.vv@icloud.com": "davidvv", |
| "wangqiang@wangqiangdeMac-mini.local": "xiaoqiang243", |
| "snreynolds2506@gmail.com": "snreynolds", |
| "35742124+0xbyt4@users.noreply.github.com": "0xbyt4", |
| "71184274+MassiveMassimo@users.noreply.github.com": "MassiveMassimo", |
| "massivemassimo@users.noreply.github.com": "MassiveMassimo", |
| "82637225+kshitijk4poor@users.noreply.github.com": "kshitijk4poor", |
| "keifergu@tencent.com": "keifergu", |
| "kshitijk4poor@users.noreply.github.com": "kshitijk4poor", |
| "abner.the.foreman@agentmail.to": "Abnertheforeman", |
| "harryykyle1@gmail.com": "hharry11", |
| "kshitijk4poor@gmail.com": "kshitijk4poor", |
| "16443023+stablegenius49@users.noreply.github.com": "stablegenius49", |
| "185121704+stablegenius49@users.noreply.github.com": "stablegenius49", |
| "101283333+batuhankocyigit@users.noreply.github.com": "batuhankocyigit", |
| "255305877+ismell0992-afk@users.noreply.github.com": "ismell0992-afk", |
| "valdi.jorge@gmail.com": "jvcl", |
| "francip@gmail.com": "francip", |
| "omni@comelse.com": "omnissiah-comelse", |
| "oussama.redcode@gmail.com": "mavrickdeveloper", |
| "126368201+vilkasdev@users.noreply.github.com": "vilkasdev", |
| "137614867+cutepawss@users.noreply.github.com": "cutepawss", |
| "96793918+memosr@users.noreply.github.com": "memosr", |
| "milkoor@users.noreply.github.com": "milkoor", |
| "xuerui911@gmail.com": "Fatty911", |
| "131039422+SHL0MS@users.noreply.github.com": "SHL0MS", |
| "77628552+raulvidis@users.noreply.github.com": "raulvidis", |
| "145567217+Aum08Desai@users.noreply.github.com": "Aum08Desai", |
| "256820943+kshitij-eliza@users.noreply.github.com": "kshitij-eliza", |
| "44278268+shitcoinsherpa@users.noreply.github.com": "shitcoinsherpa", |
| "104278804+Sertug17@users.noreply.github.com": "Sertug17", |
| "112503481+caentzminger@users.noreply.github.com": "caentzminger", |
| "258577966+voidborne-d@users.noreply.github.com": "voidborne-d", |
| "sir_even@icloud.com": "sirEven", |
| "36056348+sirEven@users.noreply.github.com": "sirEven", |
| "70424851+insecurejezza@users.noreply.github.com": "insecurejezza", |
| "254021826+dodo-reach@users.noreply.github.com": "dodo-reach", |
| "259807879+Bartok9@users.noreply.github.com": "Bartok9", |
| "241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter", |
| "268667990+Roy-oss1@users.noreply.github.com": "Roy-oss1", |
| "27917469+nosleepcassette@users.noreply.github.com": "nosleepcassette", |
| "241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter", |
| "109555139+davetist@users.noreply.github.com": "davetist", |
| "39405770+yyq4193@users.noreply.github.com": "yyq4193", |
| "Asunfly@users.noreply.github.com": "Asunfly", |
| "2500400+honghua@users.noreply.github.com": "honghua", |
| "462836+jplew@users.noreply.github.com": "jplew", |
| "nish3451@users.noreply.github.com": "nish3451", |
| "Mibayy@users.noreply.github.com": "Mibayy", |
| "mibayy@users.noreply.github.com": "Mibayy", |
| "135070653+sgaofen@users.noreply.github.com": "sgaofen", |
| "nocoo@users.noreply.github.com": "nocoo", |
| "30841158+n-WN@users.noreply.github.com": "n-WN", |
| "tsuijinglei@gmail.com": "hiddenpuppy", |
| "jerome@clawwork.ai": "HiddenPuppy", |
| "wysie@users.noreply.github.com": "Wysie", |
| "leoyuan0099@gmail.com": "keyuyuan", |
| "bxzt2006@163.com": "Only-Code-A", |
| "i@troy-y.org": "TroyMitchell911", |
| "mygamez@163.com": "zhongyueming1121", |
| "hansnow@users.noreply.github.com": "hansnow", |
| "134848055+UNLINEARITY@users.noreply.github.com": "UNLINEARITY", |
| "ben.burtenshaw@gmail.com": "burtenshaw", |
| "roopaknijhara@gmail.com": "rnijhara", |
| "josephzcan@gmail.com": "j0sephz", |
| |
| "ahmedsherif95@gmail.com": "asheriif", |
| "dyxushuai@gmail.com": "dyxushuai", |
| "33860762+etcircle@users.noreply.github.com": "etcircle", |
| "liujinkun@bytedance.com": "liujinkun2025", |
| "dmayhem93@gmail.com": "dmahan93", |
| "fr@tecompanytea.com": "ifrederico", |
| "cdanis@gmail.com": "cdanis", |
| "samherring99@gmail.com": "samherring99", |
| "desaiaum08@gmail.com": "Aum08Desai", |
| "shannon.sands.1979@gmail.com": "shannonsands", |
| "shannon@nousresearch.com": "shannonsands", |
| "abdi.moya@gmail.com": "AxDSan", |
| "eri@plasticlabs.ai": "Erosika", |
| "hjcpuro@gmail.com": "hjc-puro", |
| "xaydinoktay@gmail.com": "aydnOktay", |
| "abdullahfarukozden@gmail.com": "Farukest", |
| "lovre.pesut@gmail.com": "rovle", |
| "xjtumj@gmail.com": "mengjian-github", |
| "kevinskysunny@gmail.com": "kevinskysunny", |
| "xiewenxuan462@gmail.com": "yule975", |
| "yiweimeng.dlut@hotmail.com": "meng93", |
| "hakanerten02@hotmail.com": "teyrebaz33", |
| "linux2010@users.noreply.github.com": "Linux2010", |
| "elmatadorgh@users.noreply.github.com": "elmatadorgh", |
| "alexazzjjtt@163.com": "alexzhu0", |
| "1180176+Swift42@users.noreply.github.com": "Swift42", |
| "ruzzgarcn@gmail.com": "Ruzzgar", |
| "yukipukikedy@gmail.com": "Yukipukii1", |
| "alireza78.crypto@gmail.com": "alireza78a", |
| "brooklyn.bb.nicholson@gmail.com": "brooklynnicholson", |
| "withapurpose37@gmail.com": "StefanIsMe", |
| "4317663+helix4u@users.noreply.github.com": "helix4u", |
| "ifkellx@users.noreply.github.com": "Ifkellx", |
| "331214+counterposition@users.noreply.github.com": "counterposition", |
| "blspear@gmail.com": "BrennerSpear", |
| "akhater@gmail.com": "akhater", |
| "Cos_Admin@PTG-COS.lodluvup4uaudnm3ycd14giyug.xx.internal.cloudapp.net": "akhater", |
| "239876380+handsdiff@users.noreply.github.com": "handsdiff", |
| "hesapacicam112@gmail.com": "etherman-os", |
| "mark.ramsell@rivermounts.com": "mark-ramsell", |
| "taeng02@icloud.com": "taeng0204", |
| "gpickett00@gmail.com": "gpickett00", |
| "mcosma@gmail.com": "wakamex", |
| "clawdia.nash@proton.me": "clawdia-nash", |
| "pickett.austin@gmail.com": "austinpickett", |
| "dangtc94@gmail.com": "dieutx", |
| "jaisehgal11299@gmail.com": "jaisup", |
| "percydikec@gmail.com": "PercyDikec", |
| "noonou7@gmail.com": "HenkDz", |
| "dean.kerr@gmail.com": "deankerr", |
| "socrates1024@gmail.com": "socrates1024", |
| "seanalt555@gmail.com": "Salt-555", |
| "satelerd@gmail.com": "satelerd", |
| "dan@danlynn.com": "danklynn", |
| "mattmaximo@hotmail.com": "MattMaximo", |
| "numman.ali@gmail.com": "nummanali", |
| "rohithsaimidigudla@gmail.com": "whitehatjr1001", |
| "0xNyk@users.noreply.github.com": "0xNyk", |
| "0xnykcd@googlemail.com": "0xNyk", |
| "buraysandro9@gmail.com": "buray", |
| "contact@jomar.fr": "joshmartinelle", |
| "camilo@tekelala.com": "tekelala", |
| "vincentcharlebois@gmail.com": "vincentcharlebois", |
| "aryan@synvoid.com": "aryansingh", |
| "johnsonblake1@gmail.com": "blakejohnson", |
| "hcn518@gmail.com": "pedh", |
| "haileymarshall005@gmail.com": "haileymarshall", |
| "greer.guthrie@gmail.com": "g-guthrie", |
| "kennyx102@gmail.com": "bobashopcashier", |
| "shokatalishaikh95@gmail.com": "areu01or00", |
| "bryan@intertwinesys.com": "bryanyoung", |
| "christo.mitov@gmail.com": "christomitov", |
| "hermes@nousresearch.com": "NousResearch", |
| "hermes@noushq.ai": "benbarclay", |
| "chinmingcock@gmail.com": "ChimingLiu", |
| "openclaw@sparklab.ai": "openclaw", |
| "semihcvlk53@gmail.com": "Himess", |
| "erenkar950@gmail.com": "erenkarakus", |
| "adavyasharma@gmail.com": "adavyas", |
| "acaayush1111@gmail.com": "aayushchaudhary", |
| "jason@outland.art": "jasonoutland", |
| "73175452+Magaav@users.noreply.github.com": "Magaav", |
| "mrflu1918@proton.me": "SPANISHFLU", |
| "morganemoss@gmai.com": "mormio", |
| "kopjop926@gmail.com": "cesareth", |
| "fuleinist@gmail.com": "fuleinist", |
| "jack.47@gmail.com": "JackTheGit", |
| "dalvidjr2022@gmail.com": "Jr-kenny", |
| "m@statecraft.systems": "mbierling", |
| "balyan.sid@gmail.com": "alt-glitch", |
| "oluwadareab12@gmail.com": "bennytimz", |
| "simon@simonmarcus.org": "simon-marcus", |
| "xowiekk@gmail.com": "Xowiek", |
| "1243352777@qq.com": "zons-zhaozhy", |
| "e.silacandmr@gmail.com": "Es1la", |
| |
| |
| "1115117931@qq.com": "aaronagent", |
| "1506751656@qq.com": "hqhq1025", |
| "364939526@qq.com": "luyao618", |
| "hgk324@gmail.com": "houziershi", |
| "176644217+PStarH@users.noreply.github.com": "PStarH", |
| "51058514+Sanjays2402@users.noreply.github.com": "Sanjays2402", |
| "906014227@qq.com": "bingo906", |
| "aaronwong1999@icloud.com": "AaronWong1999", |
| "agents@kylefrench.dev": "DeployFaith", |
| "angelos@oikos.lan.home.malaiwah.com": "angelos", |
| "aptx4561@gmail.com": "cokemine", |
| "arilotter@gmail.com": "ethernet8023", |
| "ben@nousresearch.com": "benbarclay", |
| "birdiegyal@gmail.com": "yyovil", |
| "boschi1997@gmail.com": "nicoloboschi", |
| "chef.ya@gmail.com": "cherifya", |
| "chlqhdtn98@gmail.com": "BongSuCHOI", |
| "coffeemjj@gmail.com": "Cafexss", |
| "dalianmao0107@gmail.com": "dalianmao000", |
| "der@konsi.org": "konsisumer", |
| "dgrieco@redhat.com": "DomGrieco", |
| "dhicham.pro@gmail.com": "spideystreet", |
| "dipp.who@gmail.com": "dippwho", |
| "don.rhm@gmail.com": "donrhmexe", |
| "dorukardahan@hotmail.com": "dorukardahan", |
| "dsocolobsky@gmail.com": "dsocolobsky", |
| "dylan.socolobsky@lambdaclass.com": "dsocolobsky", |
| "ignacio.avecilla@lambdaclass.com": "IAvecilla", |
| "duerzy@gmail.com": "duerzy", |
| "emozilla@nousresearch.com": "emozilla", |
| "fancydirty@gmail.com": "fancydirty", |
| "farion1231@gmail.com": "farion1231", |
| "floptopbot33@gmail.com": "flobo3", |
| "fontana.pedro93@gmail.com": "pefontana", |
| "francis.x.fitzpatrick@gmail.com": "fxfitz", |
| "frank@helmschrott.de": "Helmi", |
| "gaixg94@gmail.com": "gaixianggeng", |
| "geoff.wellman@gmail.com": "geoffwellman", |
| "han.shan@live.cn": "jamesarch", |
| "haolong@microsoft.com": "LongOddCode", |
| "hata1234@gmail.com": "hata1234", |
| "hmbown@gmail.com": "Hmbown", |
| "iacobs@m0n5t3r.info": "m0n5t3r", |
| "jiayuw794@gmail.com": "JiayuuWang", |
| "jonny@nousresearch.com": "jquesnelle", |
| "juan.ovalle@mistral.ai": "jjovalle99", |
| "julien.talbot@ergonomia.re": "Julientalbot", |
| "kagura.chen28@gmail.com": "kagura-agent", |
| "1342088860@qq.com": "youngDoo", |
| "kamil@gwozdz.me": "kamil-gwozdz", |
| "skmishra1991@gmail.com": "bugkill3r", |
| "karamusti912@gmail.com": "MustafaKara7", |
| "kira@ariaki.me": "kira-ariaki", |
| "knopki@duck.com": "knopki", |
| "limars874@gmail.com": "limars874", |
| "lisicheng168@gmail.com": "lesterli", |
| "mingjwan@microsoft.com": "MagicRay1217", |
| "orangeko@gmail.com": "GenKoKo", |
| "82095453+iacker@users.noreply.github.com": "iacker", |
| "sontianye@users.noreply.github.com": "sontianye", |
| "jackjin1997@users.noreply.github.com": "jackjin1997", |
| "1037461232@qq.com": "jackjin1997", |
| "danieldoderlein@users.noreply.github.com": "danieldoderlein", |
| "lrawnsley@users.noreply.github.com": "lrawnsley", |
| "taeuk178@users.noreply.github.com": "taeuk178", |
| "ogzerber@users.noreply.github.com": "ogzerber", |
| "cola-runner@users.noreply.github.com": "cola-runner", |
| "ygd58@users.noreply.github.com": "ygd58", |
| "vominh1919@users.noreply.github.com": "vominh1919", |
| "iamagenius00@users.noreply.github.com": "iamagenius00", |
| "9219265+cresslank@users.noreply.github.com": "cresslank", |
| "trevmanthony@gmail.com": "trevthefoolish", |
| "ziliangpeng@users.noreply.github.com": "ziliangpeng", |
| "centripetal-star@users.noreply.github.com": "centripetal-star", |
| "LeonSGP43@users.noreply.github.com": "LeonSGP43", |
| "154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43", |
| "Lubrsy706@users.noreply.github.com": "Lubrsy706", |
| "niyant@spicefi.xyz": "spniyant", |
| "olafthiele@gmail.com": "olafthiele", |
| "oncuevtv@gmail.com": "sprmn24", |
| "programming@olafthiele.com": "olafthiele", |
| "r2668940489@gmail.com": "r266-tech", |
| "s5460703@gmail.com": "BlackishGreen33", |
| "saul.jj.wu@gmail.com": "SaulJWu", |
| "shenhaocheng19990111@gmail.com": "hcshen0111", |
| "sjtuwbh@gmail.com": "Cygra", |
| "srhtsrht17@gmail.com": "Sertug17", |
| "stephenschoettler@gmail.com": "stephenschoettler", |
| "tanishq231003@gmail.com": "yyovil", |
| "taosiyuan163@153.com": "taosiyuan163", |
| "tesseracttars@gmail.com": "tesseracttars-creator", |
| "tianliangjay@gmail.com": "xingkongliang", |
| "tranquil_flow@protonmail.com": "Tranquil-Flow", |
| "unayung@gmail.com": "Unayung", |
| "vorvul.danylo@gmail.com": "WorldInnovationsDepartment", |
| "win4r@outlook.com": "win4r", |
| "xush@xush.org": "KUSH42", |
| "yangzhi.see@gmail.com": "SeeYangZhi", |
| "yongtenglei@gmail.com": "yongtenglei", |
| "young@YoungdeMacBook-Pro.local": "YoungYang963", |
| "ysfalweshcan@gmail.com": "Junass1", |
| "ysfwaxlycan@gmail.com": "WAXLYY", |
| "yusufalweshdemir@gmail.com": "Dusk1e", |
| "zhouboli@gmail.com": "zhouboli", |
| "zqiao@microsoft.com": "tomqiaozc", |
| "zzn+pa@zzn.im": "xinbenlv", |
| "zaynjarvis@gmail.com": "ZaynJarvis", |
| "zhiheng.liu@bytedance.com": "ZaynJarvis", |
| "mbelleau@Michels-MacBook-Pro.local": "malaiwah", |
| "michel.belleau@malaiwah.com": "malaiwah", |
| "gnanasekaran.sekareee@gmail.com": "gnanam1990", |
| "jz.pentest@gmail.com": "0xyg3n", |
| "hypnosis.mda@gmail.com": "Hypn0sis", |
| "ywt000818@gmail.com": "OwenYWT", |
| "dhandhalyabhavik@gmail.com": "v1k22", |
| "rucchizhao@zhaochenfeideMacBook-Pro.local": "RucchiZ", |
| "tannerfokkens@Mac.attlocal.net": "tannerfokkens-maker", |
| "lehaolin98@outlook.com": "LehaoLin", |
| "yuewang1@microsoft.com": "imink", |
| "1736355688@qq.com": "hedgeho9X", |
| "bernylinville@devopsthink.org": "bernylinville", |
| "brian@bde.io": "briandevans", |
| "hubin_ll@qq.com": "LLQWQ", |
| "memosr_email@gmail.com": "memosr", |
| "anthhub@163.com": "anthhub", |
| "shenuu@gmail.com": "shenuu", |
| "xiayh17@gmail.com": "xiayh0107", |
| "zhujianxyz@gmail.com": "opriz", |
| "asurla@nvidia.com": "anniesurla", |
| "limkuan24@gmail.com": "WideLee", |
| "aviralarora002@gmail.com": "AviArora02-commits", |
| "draixagent@gmail.com": "draix", |
| "junminliu@gmail.com": "JimLiu", |
| "jarvischer@gmail.com": "maxchernin", |
| "levantam.98.2324@gmail.com": "LVT382009", |
| "zhurongcheng@rcrai.com": "heykb", |
| "withapurpose37@gmail.com": "StefanIsMe", |
| "261797239+lumenradley@users.noreply.github.com": "lumenradley", |
| "166376523+sjz-ks@users.noreply.github.com": "sjz-ks", |
| "haileymarshall005@gmail.com": "haileymarshall", |
| "aniruddhaadak80@users.noreply.github.com": "aniruddhaadak80", |
| "zheng.jerilyn@gmail.com": "jerilynzheng", |
| "asslaenn5@gmail.com": "Aslaaen", |
| "shalompmc0505@naver.com": "pinion05", |
| "105142614+VTRiot@users.noreply.github.com": "VTRiot", |
| "vivien000812@gmail.com": "iamagenius00", |
| "89228157+Feranmi10@users.noreply.github.com": "Feranmi10", |
| "simon@gtcl.us": "simon-gtcl", |
| "suzukaze.haduki@gmail.com": "houko", |
| "cliff@cigii.com": "cgarwood82", |
| "anna@oa.ke": "anna-oake", |
| "jaffarkeikei@gmail.com": "jaffarkeikei", |
| "hxp@hxp.plus": "hxp-plus", |
| "3580442280@qq.com": "Tianworld", |
| "wujianxu91@gmail.com": "wujhsu", |
| "zhrh120@gmail.com": "niyoh120", |
| "vrinek@hey.com": "vrinek", |
| "268198004+xandersbell@users.noreply.github.com": "xandersbell", |
| "somme4096@gmail.com": "Somme4096", |
| "brian@tiuxo.com": "brianclemens", |
| "25944632+yudaiyan@users.noreply.github.com": "yudaiyan", |
| "chayton@sina.com": "ycbai", |
| "longsizhuo@gmail.com": "longsizhuo", |
| "chenb19870707@gmail.com": "ms-alan", |
| "276886827+WuTianyi123@users.noreply.github.com": "WuTianyi123", |
| "22549957+li0near@users.noreply.github.com": "li0near", |
| "23434080+sicnuyudidi@users.noreply.github.com": "sicnuyudidi", |
| "haimu0x0@proton.me": "haimu0x", |
| "abdelmajidnidnasser1@gmail.com": "NIDNASSER-Abdelmajid", |
| "projectadmin@wit.id": "projectadmin-dev", |
| "mrigankamondal10@gmail.com": "Dev-Mriganka", |
| "132275809+shushuzn@users.noreply.github.com": "shushuzn", |
| "ibrahimozsarac@gmail.com": "iborazzi", |
| "130149563+A-afflatus@users.noreply.github.com": "A-afflatus", |
| "huangkwell@163.com": "huangke19", |
| "tanishq@exa.ai": "10ishq", |
| "363708+christopherwoodall@users.noreply.github.com": "christopherwoodall", |
| "zhang9w0v5@qq.com": "zhang9w0v5", |
| "fuleinist@outlook.com": "fuleinist", |
| "43494187+Llugaes@users.noreply.github.com": "Llugaes", |
| "fengtianyu88@users.noreply.github.com": "fengtianyu88", |
| "l.moncany@gmail.com": "lmoncany", |
| "fatinghenji@users.noreply.github.com": "fatinghenji", |
| "xin.peng.dr@gmail.com": "xinpengdr", |
| "mike@mikewaters.net": "mikewaters", |
| "65117428+WadydX@users.noreply.github.com": "WadydX", |
| "216480837+isaachuangGMICLOUD@users.noreply.github.com": "isaachuangGMICLOUD", |
| "nukuom976228@gmail.com": "hsy5571616", |
| "11462216+Nan93@users.noreply.github.com": "Nan93", |
| "l973401489@126.com": "zhouxiaoya12", |
| "373119611@qq.com": "roytian1217", |
| "brett@brettbrewer.com": "minorgod", |
| "67779267+wenhao7@users.noreply.github.com": "wenhao7", |
| "git@yzx9.xyz": "yzx9", |
| "nilesh@cloudgeni.us": "lvnilesh", |
| "63502660+azhengbot@users.noreply.github.com": "azhengbot", |
| "sharvil.saxena@gmail.com": "sharziki", |
| "yuanhe@minimaxi.com": "RyanLee-Dev", |
| "curtis992250@gmail.com": "TaroballzChen", |
| "92638503+Lind3ey@users.noreply.github.com": "Lind3ey", |
| "1352808998@qq.com": "phpoh", |
| "caliberoviv@gmail.com": "vivganes", |
| "michaelfackerell@gmail.com": "MikeFac", |
| "18024642@qq.com": "GuyCui", |
| "eumael.mkt@gmail.com": "maelrx", |
| |
| "benbarclay@gmail.com": "benbarclay", |
| "lijiawen@umich.edu": "Jiawen-lee", |
| "oleksiy@kovyrin.net": "kovyrin", |
| "kovyrin.claw@gmail.com": "kovyrin", |
| "kaiobarb@gmail.com": "liftaris", |
| "me@arihantsethia.com": "arihantsethia", |
| "zhuofengwang2003@gmail.com": "coekfung", |
| "teknium@noreply.github.com": "teknium1", |
| "2114364329@qq.com": "cuyua9", |
| "2557058999@qq.com": "Disaster-Terminator", |
| "cine.dreamer.one@gmail.com": "LeonSGP43", |
| "leozeli@qq.com": "leozeli", |
| "linlehao@cuhk.edu.cn": "LehaoLin", |
| "liutong@isacas.ac.cn": "I3eg1nner", |
| "peterberthelsen@Peters-MacBook-Air.local": "PeterBerthelsen", |
| "root@debian.debian": "lengxii", |
| "roque@priveperfumeshn.com": "priveperfumes", |
| "shijianzhi@shijianzhideMacBook-Pro.local": "sjz-ks", |
| "topcheer@me.com": "topcheer", |
| "walli@tencent.com": "walli", |
| "zhuofengwang@tencent.com": "Zhuofeng-Wang", |
| |
| "clio-agent@sisyphuslabs.ai": "Sisyphus", |
| "marco@rutimka.de": "Marco Rutsch", |
| "paul@gamma.app": "Paul Bergeron", |
| "zhangxicen@example.com": "zhangxicen", |
| "codex@openai.invalid": "teknium1", |
| "screenmachine@gmail.com": "teknium1", |
| } |
|
|
|
|
| def git(*args, cwd=None): |
| """Run a git command and return stdout.""" |
| result = subprocess.run( |
| ["git"] + list(args), |
| capture_output=True, text=True, |
| cwd=cwd or str(REPO_ROOT), |
| ) |
| if result.returncode != 0: |
| print(f"git {' '.join(args)} failed: {result.stderr}", file=sys.stderr) |
| return "" |
| return result.stdout.strip() |
|
|
|
|
| def git_result(*args, cwd=None): |
| """Run a git command and return the full CompletedProcess.""" |
| return subprocess.run( |
| ["git"] + list(args), |
| capture_output=True, |
| text=True, |
| cwd=cwd or str(REPO_ROOT), |
| ) |
|
|
|
|
| def get_last_tag(): |
| """Get the most recent CalVer tag.""" |
| tags = git("tag", "--list", "v20*", "--sort=-v:refname") |
| if tags: |
| return tags.split("\n")[0] |
| return None |
|
|
|
|
| def next_available_tag(base_tag: str) -> tuple[str, str]: |
| """Return a tag/calver pair, suffixing same-day releases when needed.""" |
| if not git("tag", "--list", base_tag): |
| return base_tag, base_tag.removeprefix("v") |
|
|
| suffix = 2 |
| while git("tag", "--list", f"{base_tag}.{suffix}"): |
| suffix += 1 |
| tag_name = f"{base_tag}.{suffix}" |
| return tag_name, tag_name.removeprefix("v") |
|
|
|
|
| def get_current_version(): |
| """Read current semver from __init__.py.""" |
| content = VERSION_FILE.read_text() |
| match = re.search(r'__version__\s*=\s*"([^"]+)"', content) |
| return match.group(1) if match else "0.0.0" |
|
|
|
|
| def bump_version(current: str, part: str) -> str: |
| """Bump a semver version string.""" |
| parts = current.split(".") |
| if len(parts) != 3: |
| parts = ["0", "0", "0"] |
| major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2]) |
|
|
| if part == "major": |
| major += 1 |
| minor = 0 |
| patch = 0 |
| elif part == "minor": |
| minor += 1 |
| patch = 0 |
| elif part == "patch": |
| patch += 1 |
| else: |
| raise ValueError(f"Unknown bump part: {part}") |
|
|
| return f"{major}.{minor}.{patch}" |
|
|
|
|
| def update_version_files(semver: str, calver_date: str): |
| """Update version strings in source files.""" |
| |
| content = VERSION_FILE.read_text() |
| content = re.sub( |
| r'__version__\s*=\s*"[^"]+"', |
| f'__version__ = "{semver}"', |
| content, |
| ) |
| content = re.sub( |
| r'__release_date__\s*=\s*"[^"]+"', |
| f'__release_date__ = "{calver_date}"', |
| content, |
| ) |
| VERSION_FILE.write_text(content) |
|
|
| |
| pyproject = PYPROJECT_FILE.read_text() |
| pyproject = re.sub( |
| r'^version\s*=\s*"[^"]+"', |
| f'version = "{semver}"', |
| pyproject, |
| flags=re.MULTILINE, |
| ) |
| PYPROJECT_FILE.write_text(pyproject) |
|
|
|
|
| def build_release_artifacts(semver: str) -> list[Path]: |
| """Build sdist/wheel artifacts for the current release. |
| |
| Returns the artifact paths when the local environment has ``python -m build`` |
| available. If build tooling is missing or the build fails, returns an empty |
| list and lets the release proceed without attached Python artifacts. |
| """ |
| dist_dir = REPO_ROOT / "dist" |
| shutil.rmtree(dist_dir, ignore_errors=True) |
|
|
| result = subprocess.run( |
| [sys.executable, "-m", "build", "--sdist", "--wheel"], |
| cwd=str(REPO_ROOT), |
| capture_output=True, |
| text=True, |
| ) |
| if result.returncode != 0: |
| print(" β Could not build Python release artifacts.") |
| stderr = result.stderr.strip() |
| stdout = result.stdout.strip() |
| if stderr: |
| print(f" {stderr.splitlines()[-1]}") |
| elif stdout: |
| print(f" {stdout.splitlines()[-1]}") |
| print(" Install the 'build' package to attach semver-named sdist/wheel assets.") |
| return [] |
|
|
| artifacts = sorted(p for p in dist_dir.iterdir() if p.is_file()) |
| matching = [p for p in artifacts if semver in p.name] |
| if not matching: |
| print(" β Built artifacts did not match the expected release version.") |
| return [] |
| return matching |
|
|
|
|
| def resolve_author(name: str, email: str) -> str: |
| """Resolve a git author to a GitHub @mention.""" |
| |
| gh_user = AUTHOR_MAP.get(email) |
| if gh_user: |
| return f"@{gh_user}" |
|
|
| |
| noreply_match = re.match(r"(\d+)\+(.+)@users\.noreply\.github\.com", email) |
| if noreply_match: |
| return f"@{noreply_match.group(2)}" |
|
|
| |
| noreply_match2 = re.match(r"(.+)@users\.noreply\.github\.com", email) |
| if noreply_match2: |
| return f"@{noreply_match2.group(1)}" |
|
|
| |
| return name |
|
|
|
|
| def categorize_commit(subject: str) -> str: |
| """Categorize a commit by its conventional commit prefix.""" |
| subject_lower = subject.lower() |
|
|
| |
| patterns = { |
| "breaking": [r"^breaking[\s:(]", r"^!:", r"BREAKING CHANGE"], |
| "features": [r"^feat[\s:(]", r"^feature[\s:(]", r"^add[\s:(]"], |
| "fixes": [r"^fix[\s:(]", r"^bugfix[\s:(]", r"^bug[\s:(]", r"^hotfix[\s:(]"], |
| "improvements": [r"^improve[\s:(]", r"^perf[\s:(]", r"^enhance[\s:(]", |
| r"^refactor[\s:(]", r"^cleanup[\s:(]", r"^clean[\s:(]", |
| r"^update[\s:(]", r"^optimize[\s:(]"], |
| "docs": [r"^doc[\s:(]", r"^docs[\s:(]"], |
| "tests": [r"^test[\s:(]", r"^tests[\s:(]"], |
| "chore": [r"^chore[\s:(]", r"^ci[\s:(]", r"^build[\s:(]", |
| r"^deps[\s:(]", r"^bump[\s:(]"], |
| } |
|
|
| for category, regexes in patterns.items(): |
| for regex in regexes: |
| if re.match(regex, subject_lower): |
| return category |
|
|
| |
| if any(w in subject_lower for w in ["add ", "new ", "implement", "support "]): |
| return "features" |
| if any(w in subject_lower for w in ["fix ", "fixed ", "resolve", "patch "]): |
| return "fixes" |
| if any(w in subject_lower for w in ["refactor", "cleanup", "improve", "update "]): |
| return "improvements" |
|
|
| return "other" |
|
|
|
|
| def clean_subject(subject: str) -> str: |
| """Clean up a commit subject for display.""" |
| |
| 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) |
| |
| cleaned = cleaned.strip() |
| |
| if cleaned: |
| cleaned = cleaned[0].upper() + cleaned[1:] |
| return cleaned |
|
|
|
|
| def parse_coauthors(body: str) -> list: |
| """Extract Co-authored-by trailers from a commit message body. |
| |
| Returns a list of {'name': ..., 'email': ...} dicts. |
| Filters out AI assistants and bots (Claude, Copilot, Cursor, etc.). |
| """ |
| if not body: |
| return [] |
| |
| _ignored_emails = {"noreply@anthropic.com", "noreply@github.com", |
| "cursoragent@cursor.com", "hermes@nousresearch.com"} |
| _ignored_names = re.compile(r"^(Claude|Copilot|Cursor Agent|GitHub Actions?|dependabot|renovate)", re.IGNORECASE) |
| pattern = re.compile(r"Co-authored-by:\s*(.+?)\s*<([^>]+)>", re.IGNORECASE) |
| results = [] |
| for m in pattern.finditer(body): |
| name, email = m.group(1).strip(), m.group(2).strip() |
| if email in _ignored_emails or _ignored_names.match(name): |
| continue |
| results.append({"name": name, "email": email}) |
| return results |
|
|
|
|
| def get_commits(since_tag=None): |
| """Get commits since a tag (or all commits if None).""" |
| if since_tag: |
| range_spec = f"{since_tag}..HEAD" |
| else: |
| range_spec = "HEAD" |
|
|
| |
| |
| log = git( |
| "log", range_spec, |
| "--format=%H|%an|%ae|%s%x00%b%x00", |
| "--no-merges", |
| ) |
|
|
| if not log: |
| return [] |
|
|
| commits = [] |
| |
| |
| for entry in log.split("\0\0"): |
| entry = entry.strip() |
| if not entry: |
| continue |
| |
| if "\0" in entry: |
| header, body = entry.split("\0", 1) |
| body = body.strip() |
| else: |
| header = entry |
| body = "" |
| parts = header.split("|", 3) |
| if len(parts) != 4: |
| continue |
| sha, name, email, subject = parts |
| coauthor_info = parse_coauthors(body) |
| coauthors = [resolve_author(ca["name"], ca["email"]) for ca in coauthor_info] |
| commits.append({ |
| "sha": sha, |
| "short_sha": sha[:8], |
| "author_name": name, |
| "author_email": email, |
| "subject": subject, |
| "category": categorize_commit(subject), |
| "github_author": resolve_author(name, email), |
| "coauthors": coauthors, |
| }) |
|
|
| return commits |
|
|
|
|
| def get_pr_number(subject: str) -> str: |
| """Extract PR number from commit subject if present.""" |
| match = re.search(r"#(\d+)", subject) |
| if match: |
| return match.group(1) |
| return None |
|
|
|
|
| def generate_changelog(commits, tag_name, semver, repo_url="https://github.com/NousResearch/hermes-agent", |
| prev_tag=None, first_release=False): |
| """Generate markdown changelog from categorized commits.""" |
| lines = [] |
|
|
| |
| now = datetime.now() |
| date_str = now.strftime("%B %d, %Y") |
| lines.append(f"# Hermes Agent v{semver} ({tag_name})") |
| lines.append("") |
| lines.append(f"**Release Date:** {date_str}") |
| lines.append("") |
|
|
| if first_release: |
| lines.append("> π **First official release!** This marks the beginning of regular weekly releases") |
| lines.append("> for Hermes Agent. See below for everything included in this initial release.") |
| lines.append("") |
|
|
| |
| categories = defaultdict(list) |
| all_authors = set() |
| teknium_aliases = {"@teknium1"} |
|
|
| for commit in commits: |
| categories[commit["category"]].append(commit) |
| author = commit["github_author"] |
| if author not in teknium_aliases: |
| all_authors.add(author) |
| for coauthor in commit.get("coauthors", []): |
| if coauthor not in teknium_aliases: |
| all_authors.add(coauthor) |
|
|
| |
| category_order = [ |
| ("breaking", "β οΈ Breaking Changes"), |
| ("features", "β¨ Features"), |
| ("improvements", "π§ Improvements"), |
| ("fixes", "π Bug Fixes"), |
| ("docs", "π Documentation"), |
| ("tests", "π§ͺ Tests"), |
| ("chore", "ποΈ Infrastructure"), |
| ("other", "π¦ Other Changes"), |
| ] |
|
|
| for cat_key, cat_title in category_order: |
| cat_commits = categories.get(cat_key, []) |
| if not cat_commits: |
| continue |
|
|
| lines.append(f"## {cat_title}") |
| lines.append("") |
|
|
| for commit in cat_commits: |
| subject = clean_subject(commit["subject"]) |
| pr_num = get_pr_number(commit["subject"]) |
| author = commit["github_author"] |
|
|
| |
| parts = [f"- {subject}"] |
| if pr_num: |
| parts.append(f"([#{pr_num}]({repo_url}/pull/{pr_num}))") |
| else: |
| parts.append(f"([`{commit['short_sha']}`]({repo_url}/commit/{commit['sha']}))") |
|
|
| if author not in teknium_aliases: |
| parts.append(f"β {author}") |
|
|
| lines.append(" ".join(parts)) |
|
|
| lines.append("") |
|
|
| |
| if all_authors: |
| |
| author_counts = defaultdict(int) |
| for commit in commits: |
| author = commit["github_author"] |
| if author not in teknium_aliases: |
| author_counts[author] += 1 |
| for coauthor in commit.get("coauthors", []): |
| if coauthor not in teknium_aliases: |
| author_counts[coauthor] += 1 |
|
|
| sorted_authors = sorted(author_counts.items(), key=lambda x: -x[1]) |
|
|
| lines.append("## π₯ Contributors") |
| lines.append("") |
| lines.append("Thank you to everyone who contributed to this release!") |
| lines.append("") |
| for author, count in sorted_authors: |
| commit_word = "commit" if count == 1 else "commits" |
| lines.append(f"- {author} ({count} {commit_word})") |
| lines.append("") |
|
|
| |
| if prev_tag: |
| lines.append(f"**Full Changelog**: [{prev_tag}...{tag_name}]({repo_url}/compare/{prev_tag}...{tag_name})") |
| else: |
| lines.append(f"**Full Changelog**: [{tag_name}]({repo_url}/commits/{tag_name})") |
| lines.append("") |
|
|
| return "\n".join(lines) |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Hermes Agent Release Tool") |
| parser.add_argument("--bump", choices=["major", "minor", "patch"], |
| help="Which semver component to bump") |
| parser.add_argument("--publish", action="store_true", |
| help="Actually create the tag and GitHub release (otherwise dry run)") |
| parser.add_argument("--date", type=str, |
| help="Override CalVer date (format: YYYY.M.D)") |
| parser.add_argument("--first-release", action="store_true", |
| help="Mark as first release (no previous tag expected)") |
| parser.add_argument("--output", type=str, |
| help="Write changelog to file instead of stdout") |
| args = parser.parse_args() |
|
|
| |
| if args.date: |
| calver_date = args.date |
| else: |
| now = datetime.now() |
| calver_date = f"{now.year}.{now.month}.{now.day}" |
|
|
| base_tag = f"v{calver_date}" |
| tag_name, calver_date = next_available_tag(base_tag) |
| if tag_name != base_tag: |
| print(f"Note: Tag {base_tag} already exists, using {tag_name}") |
|
|
| |
| current_version = get_current_version() |
| if args.bump: |
| new_version = bump_version(current_version, args.bump) |
| else: |
| new_version = current_version |
|
|
| |
| prev_tag = get_last_tag() |
| if not prev_tag and not args.first_release: |
| print("No previous tags found. Use --first-release for the initial release.") |
| print(f"Would create tag: {tag_name}") |
| print(f"Would set version: {new_version}") |
|
|
| |
| commits = get_commits(since_tag=prev_tag) |
| if not commits: |
| print("No new commits since last tag.") |
| if not args.first_release: |
| return |
|
|
| print(f"{'='*60}") |
| print(f" Hermes Agent Release Preview") |
| print(f"{'='*60}") |
| print(f" CalVer tag: {tag_name}") |
| print(f" SemVer: v{current_version} β v{new_version}") |
| print(f" Previous tag: {prev_tag or '(none β first release)'}") |
| print(f" Commits: {len(commits)}") |
| print(f" Unique authors: {len(set(c['github_author'] for c in commits))}") |
| print(f" Mode: {'PUBLISH' if args.publish else 'DRY RUN'}") |
| print(f"{'='*60}") |
| print() |
|
|
| |
| changelog = generate_changelog( |
| commits, tag_name, new_version, |
| prev_tag=prev_tag, |
| first_release=args.first_release, |
| ) |
|
|
| if args.output: |
| Path(args.output).write_text(changelog) |
| print(f"Changelog written to {args.output}") |
| else: |
| print(changelog) |
|
|
| if args.publish: |
| print(f"\n{'='*60}") |
| print(" Publishing release...") |
| print(f"{'='*60}") |
|
|
| |
| if args.bump: |
| update_version_files(new_version, calver_date) |
| print(f" β Updated version files to v{new_version} ({calver_date})") |
|
|
| |
| add_result = git_result("add", str(VERSION_FILE), str(PYPROJECT_FILE)) |
| if add_result.returncode != 0: |
| print(f" β Failed to stage version files: {add_result.stderr.strip()}") |
| return |
|
|
| commit_result = git_result( |
| "commit", "-m", f"chore: bump version to v{new_version} ({calver_date})" |
| ) |
| if commit_result.returncode != 0: |
| print(f" β Failed to commit version bump: {commit_result.stderr.strip()}") |
| return |
| print(f" β Committed version bump") |
|
|
| |
| tag_result = git_result( |
| "tag", "-a", tag_name, "-m", |
| f"Hermes Agent v{new_version} ({calver_date})\n\nWeekly release" |
| ) |
| if tag_result.returncode != 0: |
| print(f" β Failed to create tag {tag_name}: {tag_result.stderr.strip()}") |
| return |
| print(f" β Created tag {tag_name}") |
|
|
| |
| push_result = git_result("push", "origin", "HEAD", "--tags") |
| if push_result.returncode == 0: |
| print(f" β Pushed to origin") |
| else: |
| print(f" β Failed to push to origin: {push_result.stderr.strip()}") |
| print(" Continue manually after fixing access:") |
| print(" git push origin HEAD --tags") |
|
|
| |
| |
| artifacts = build_release_artifacts(new_version) |
| if artifacts: |
| print(" β Built release artifacts:") |
| for artifact in artifacts: |
| print(f" - {artifact.relative_to(REPO_ROOT)}") |
|
|
| |
| changelog_file = REPO_ROOT / ".release_notes.md" |
| changelog_file.write_text(changelog) |
|
|
| gh_cmd = [ |
| "gh", "release", "create", tag_name, |
| "--title", f"Hermes Agent v{new_version} ({calver_date})", |
| "--notes-file", str(changelog_file), |
| ] |
| gh_cmd.extend(str(path) for path in artifacts) |
|
|
| gh_bin = shutil.which("gh") |
| if gh_bin: |
| result = subprocess.run( |
| gh_cmd, |
| capture_output=True, text=True, |
| cwd=str(REPO_ROOT), |
| ) |
| else: |
| result = None |
|
|
| if result and result.returncode == 0: |
| changelog_file.unlink(missing_ok=True) |
| print(f" β GitHub release created: {result.stdout.strip()}") |
| print(f"\n π Release v{new_version} ({tag_name}) published!") |
| else: |
| if result is None: |
| print(" β GitHub release skipped: `gh` CLI not found.") |
| else: |
| print(f" β GitHub release failed: {result.stderr.strip()}") |
| print(f" Release notes kept at: {changelog_file}") |
| print(f" Tag was created locally. Create the release manually:") |
| print( |
| f" gh release create {tag_name} --title 'Hermes Agent v{new_version} ({calver_date})' " |
| f"--notes-file .release_notes.md {' '.join(str(path) for path in artifacts)}" |
| ) |
| print(f"\n β Release artifacts prepared for manual publish: v{new_version} ({tag_name})") |
| else: |
| print(f"\n{'='*60}") |
| print(f" Dry run complete. To publish, add --publish") |
| print(f" Example: python scripts/release.py --bump minor --publish") |
| print(f"{'='*60}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|