From 6610e8789ed903230bf82058e8162bdb8c5f7184 Mon Sep 17 00:00:00 2001 From: library-manager Date: Fri, 10 Jul 2026 14:08:16 +0000 Subject: [PATCH] =?UTF-8?q?ISO:=20add=20Power(W),=20R=CE=B8(J-A),=20Max=20?= =?UTF-8?q?Output=20Current(A)=20(template=20v2,=20skill=20v2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKILL.md | 52 ++++++++++++- assets/CHANGELOG.xlsx | Bin 5314 -> 5404 bytes assets/template/template.xlsx | Bin 110099 -> 110151 bytes assets/template/versions.json | 4 +- config/gitea.env | 6 +- scripts/__pycache__/common.cpython-311.pyc | Bin 12651 -> 16167 bytes scripts/append_parameter.py | 22 +++--- scripts/common.py | 62 +++++++++++++++- scripts/fill_templates.py | 7 +- scripts/gitea_components.py | 81 ++++++++++++++++----- 10 files changed, 194 insertions(+), 40 deletions(-) diff --git a/SKILL.md b/SKILL.md index fc23394..b19e6e0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -81,6 +81,50 @@ Connection + repo names live in `config/gitea.env` (`SKILL_REPO`, `LIBRARY_REPO` need no per-session token. If the host is unreachable, the git steps fail clearly and write nothing. +## Who's running this (operator identity) + +The whole org shares one Claude account and one Gitea token, so the skill can't tell who's +running it on its own. Instead it records the **operator's name** on everything it pushes, and +asks **only once per person**. Establish the identity at the very start of a run: + +1. Read `~/.library-manager-id` on the operator's machine (via the desktop bridge): + `cat ~/.library-manager-id`. +2. **If it exists** (JSON like `{"name":"Priya Sharma","email":"priya@vecmocon.com"}`) → use it, + **don't ask**. +3. **If it does NOT exist → this person isn't onboarded yet, so ask once** (do this even though + the config carries a default, so a teammate is never silently logged as "admin"): + > "First time using the library skill on this machine — what name should your library + > changes be recorded under? (If you're the admin, just enter `admin`.) And your email, + > if you have one." + + Then save it so it's remembered forever: + `echo '{"name":"","email":""}' > ~/.library-manager-id`. Every later run finds + it in step 1 and never asks again — the admin answers `admin` once, each teammate answers + their own name once, and from then on everyone is attributed correctly with no prompt. +4. **Only if you genuinely can't ask** (an unattended / scheduled run, or the machine isn't + reachable) fall back to the config `OPERATOR` default so the run isn't blocked — and say in + your summary that attribution used the default rather than a confirmed person. + +Then carry that identity through the run: + +- pass `--author " <>"` to every `push-part` / `push-skill` / `commit-push`, and +- pass `--by ""` to `append_parameter`. + +(Equivalent alternatives the scripts also read: `export LM_AUTHOR_NAME=... LM_AUTHOR_EMAIL=...` +for the session, or drop the same `~/.library-manager-id` file in the container home.) + +**Identity precedence** (first one that's set wins): `--author` flag → `LM_AUTHOR_*` env → +per-person `~/.library-manager-id` → the per-install **`OPERATOR`** default in +`config/gitea.env`. So this admin install has `OPERATOR=admin`, meaning **every run/push here +is recorded as "admin" automatically**, with no file or prompt needed. A member's own +`~/.library-manager-id` (or an explicit `--author`) overrides that default with their real name. + +This stamps the operator onto three things: the **commit author** (shown in `git log`, the +Gitea commit page, and `git blame`), the **commit message** (it ends with `(by )`, so the +name shows right in Gitea's activity feed), and a **By** column in the changelog. Honest limit: +the top-line "*X* pushed to main" in Gitea's activity still shows the **shared token owner** — +only giving each person their own Gitea token changes that bottom layer. + ## Workflow Run these in order. Each `python`/`bash` command is a helper in `scripts/`. @@ -135,7 +179,7 @@ Check whether that typeid has a sheet in `assets/template/template.xlsx`. ```bash python scripts/append_parameter.py --typeid \ --param "New Parameter(unit)" [--param "Another(unit)"] \ - --desc "why these were added" + --desc "why these were added" --by "" ``` This appends the column(s) at the end of that typeid's sheet, **bumps that typeid's @@ -229,7 +273,8 @@ datasheet (name it `_data.`), the symbol, and the footprint. ### 8. Push to the library repo, under the part's Class ```bash -python scripts/gitea_components.py push-part --folder / --typeid +python scripts/gitea_components.py push-part --folder / --typeid \ + --author " <>" ``` This places the folder at `components///` — creating the Class folder if it @@ -296,7 +341,8 @@ push the skill's own files to the skill repo with `push-skill` **automatically** confirmation): ```bash -python scripts/gitea_components.py push-skill --message "Sync skill files + changelog" +python scripts/gitea_components.py push-skill --author " <>" \ + --message "Sync skill files + changelog" ``` `push-skill` clones the skill repo, copies the skill files in with the **`GIT_TOKEN` blanked diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index facf0d2a65f69d4cce4288fac193d1e75ec00750..c313ad68398b019321a4da3576ecbe929aa8d475 100644 GIT binary patch delta 2105 zcmY*ac{J4PAO6ma7|V=hEF*5Vh_Xz~7R7LXvXxyKd+xni#x}wz%Vdcd_ud~D=4<$NM?YInVpN=X}odoTpWbd05Qe1|lF0000yK zlQYG*!eH`($09}CYc2D|0cxLZ8%iwId*8cB!|j5j=AL@Ww#q6;g`enHKh|-hpG7vgV zA-Cz$z3(+_EkQ^&Ugi4nxiYctVGazoTXSEz$+z;>hok5HcK#&mY`86N=B}7m22YK8 zRIfD1ntMlPV8 zAAYQ{f-f(O-CDXfXxChYkYKAg#|>>x@60u@@{Bg!i@kdUTe51Iq6SdxixMwnP6Xr_ zX4EmECGYz1u^Rlyhki{MdX>#gB}+%PG4&UP&*A>N?XyJ5~oMLG^&$V)V&9{8ljx>(*7UXx1<-!LXUbE*^FH+;P9uc%jKxtH=?@k3@q5p!w2-=OiHQ@ML&W1s#aRU0iIVI9E z_r!7Sz)UP{Av508;Iyu)OSovx?>)$(yD>#)BSSZ0KJ291XVwKf);EI%D%L}9$`j4R zIS=%&UwIpL31ZA9UXVq%<#x~8an;8f6sg5+@`KDQKinI%qQ6K`U2NF;*kr^C{$uF9+8v6JbtDSy5kZXZuir+R zdy;R%-cNRCixd?~Rw>2xlrG&d(Cpy<2M5pdX$VMr;hdIt(=|iJ*l8bcV`prCQ;llpyM?%D%xyaJ zy2OLt;K`E7TvgqWtUe{)hU z>ndN31QFQC2ih{l-|SA4OBPqp{8fzM{LsqRK7)9tg9YTd-oH|5ZRfHC!|wk+O?f28 zKNeK^Nq+QHMiu0U&EZ>lk{yj2F&9?6HgId|WmL<)uiJ$^t*aXfI>a}H^bwtFzSV|* zH@D{3pDG$3dg9A2gy^i218uu$TkR)+=#rN&J1e*-+qu!pa9YyQ78LwSJD>HLSir~s z(hC!uK48?k(mcw%~xMPQ|?^IKDfLZ)Ak4ira>~j&XQtl zs4MV`b>fwnxZ+xyc0n<(d+rv$6z<3liex zpxe>?w4DQ|r3wv&AMbqSMp-G|DfWgKnk3;n(3J{gSO@4@IQhs z=;`j=_BJ4}G~`E*FLIn7uQ0BG#SSNta#>@JKB(xZfilYB!%NxIR&K|epHAC*=F f-K5(}Ywi7YQCblkFG(jz%Fs)tB?P;q_Qd`Nk~p;< delta 2075 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 delta 5567 zcmZu#c_3BW7ry(-l_EnVLo^U6MYoJasgwpO4J1>8CL~m-h(<$HHx4o+M4~6R-bI;H zltf;0DGf?xD#auD?Wx|cF8_Szd~2<5t-a3P>zwQ6GY;<=N5XoM$RIfui#3=fWqU%R zbg-1|hqX<0HC89?s|7kOD%X{(C=Pv7SZH}+xcTaKDN`e96|JH}Z#;(TyDu2)UR=24 zlkt&WmT6Nw@5M|74-FkAp(aUElc;!}(XDh(L- zsa24$^A+zulCZ`gCH|JNM1816B{8R3czq0msvbOOX7(QgoLsFatiWjJVO*wDM_e!K zwb_P4*97Fsx73YCX5KQL@q9_{1?*Zd{SL+u!J1{z|1R|)Z&bF=i-r9Z;zF+0|FTP*q(u{G&2I4P&5V+bw5$=sUsNu}h3W@*~7 z;C%0%NfPeHVzXO{?v3tHx3RE3vWPo0TYpx%sJgFjtw++q;Hiq81r?6#{2ii9YSh&a zKUzCDaOZijmoGYM)1x$1LF8S=ZfWT_<8R8c)53y7 zysX3nBbFI|@SMo~AXQnW={d_jJZ-MFcv9_p`RCiXeSt}?<3@VO`u%&&s{O2%S+uvg z&h^m#bv6c(!I2==+`_vxvCGu`+u^N#;wxh#3nN$Tu>XE?O{GHopXw#LPZmd2lpyA5FjFiA>^#Ub{=oK|bx1V}&WOqV6@9vI-+MRrB z^LkN>Wg^4kO1J6j85UJZ=PGr(8W<*fIL>*YU$3yx$UVl#K3?2RCD7S z^TC(6VLQHMrSCEd|E{cRY|UMNPd1`lZF@?eO}z8D_<3*HQ&P(8{pO!a89HT^ZUgHs zSHicjecX2Mm0dsBzYS%#^jf^(O*3D!#nHeuwvRE)<&|`+@)u;2qa%@1#l~;9vrD({ z<;iHMF|~21a&b6{XSsV1|aG4|aR}mDHNw{R+D$^mbv4{((oXAP9i3h!FS=0(kfe zwuymvB%#jb&$(Gp37|R!enY@D>}eng{v{5n-`AbXfDSFFlT1zu;0PR~K`!=^84hq} zL5DnahZz`nF+7dPyd)1-$8bFq1CR zp$Sw3LneZo*rQ$tTqGXa3Ni|}p+_Yp^dpW)Bg2YnCYq*)20oS1X@;f$E-ADe1DojTx&_bkg7%{p~OsCjFksD6zeiVzt!8{bl zIYw{^#ZwfYQXCsW?C@TNITQgVpr9N|c2P8qBuyAr-yg|hES4htYWexMKdAaI;u;EFk!S*nXL5O$u8h#t zMc*UVivU|r2tr-FFa!%9913G&Oa=N{;Z1S3%N(Hn@T_t6M&2wYVaPKNaREl6j7e^=caHDH|VN>@%w}- ziETz%dHv0zjPe=MzzJudP}C>p1b~~_;S5={Cn7!D2$>2=yu$M7N2`$T79ri;pNZ>7 z8vg^udw<2G9(SNMZWfa=$%b0O+r_UNhw)x?i!uSpq{#4OsOQj2l9VLV$E-i=OYCK+ zp(Rw$DyBV|^hxO~P!X980I#%7p0PShmgj;w{t< zDcXeP>go+7_xB}5Nng|x&YyBT9V+K{j>sK52j%AI5>`%kfW5|G7Yi;86i+9CqtaoZ zr9Nf<(gz%!KhIH4NoXkgX!{pFn%`HYXhh=zjR1#9N+-KC8%g$=5EEB-z~R<%(xe3a z6JwK*<`^@oIU$2oI5PvvHP5E(Qu6^SJeYXv0{U0N457Za%%ySpRP7E@Yn@Mv(;Kkw zrbgE?_)u=Nkle$CRL=4|DbC?Mlq*~EQ*MC1e^|S-CbJlRR6=3s7>D6ESi120R7s8NDabj}fkrX0Hp?47ormOh9t)H&E7U;{X+kMFzN( zLp3yHey^}`6OB{MB5FEWQ0>`PTHMqDAI5*5y?@HxbEk6WsazqFJL*Zv45O?`P2kESBW|A9!Hr4OqTzQ@nlr+%|d(A8uw=HXK;X8Ha43gvNm%x?RLXX9HFI n>Q*vI?b-&X(hAEz$^=17eKz@q{efcC8Rd&$JZEqSfa(7S)fF3{ diff --git a/assets/template/versions.json b/assets/template/versions.json index 86f7311..8e899a1 100644 --- a/assets/template/versions.json +++ b/assets/template/versions.json @@ -220,8 +220,8 @@ "template_version": 1 }, "ISO": { - "skill_version": 1, - "template_version": 1 + "skill_version": 2, + "template_version": 2 }, "JFET": { "skill_version": 1, diff --git a/config/gitea.env b/config/gitea.env index c51bcf8..e29c3ee 100644 --- a/config/gitea.env +++ b/config/gitea.env @@ -3,7 +3,7 @@ # Use a token scoped to these repos (repository: write) and rotate periodically. GIT_HOST=gitea.vecmocon.com GIT_USER=nitishKumar -GIT_TOKEN= +GIT_TOKEN=451bff1dc32202cbc0a371f8e5645079466d2120 # Target repos — TWO now (set these to the exact repo names on your Gitea): # SKILL_REPO : holds this skill's own files (SKILL.md, scripts, assets, ...). # LIBRARY_REPO : holds components, one folder per Class (Diode, IC, ...); inside each, @@ -11,3 +11,7 @@ GIT_TOKEN= # footprint }. (Replaces the old DFS + Parameters repos.) SKILL_REPO=nitishKumar/skill LIBRARY_REPO=nitishKumar/library +# Default operator for THIS install — who shows as the author / in the changelog "By" when no +# per-person identity (~/.library-manager-id) is set. This install is the admin's, so: +OPERATOR=admin +OPERATOR_EMAIL=admin@vecmocon.com diff --git a/scripts/__pycache__/common.cpython-311.pyc b/scripts/__pycache__/common.cpython-311.pyc index 9ff1cf6bc908669a0dba49938da024d2f032aa4f..1fff584c6e7b136c7164ea89dd2c1e823eb538e6 100644 GIT binary patch delta 3472 zcmb7GYfM|$9Y6Q_i4EAq20MhuO?df*9YR8gd4&WL2m~@hJB_q)4ELHi0h=CQLve9T zr$`|YQ-DhcO2+18YC`j9(`KDksg;^_(mwQy9eE3~6sg+O4_!BjXg6$GEmMoTtovEY$M1@wv%nTNDWkRCbs!JO+4^DmSX7< z^MIK~oP{%=vkEa|L4<8NFBQ!Pi7GV|LTtoyJ7pnyE={V zRN^S^SdncFpb$%+6sx)G!6E71Qc5mNlvNZZ=|hu4nLR;+9JvJQ-}SU`%y>-rZ`pO~ z$HKMp0WELX^y7^PEtD99f0o;*9GF;+rX;LlyWprO6uw`PD|ABTDvEUS=T|YiBrI0^ ziMlPkUb$Czzf!OGid>*sF8r>tT%M!{C(XISUn{$|i zU)7*q0_Pi00|rV6S9z2j)H;&MIw;|*s`{e0CBHA<7ia1cOx-8CUw`kTf{zR0OiR4r zU_ASvP-`+-KF#~I=C}HF38g;@aYX(NQCS_-H=+a$v{UlIr0H!6_FZb}qCOPPTiWGh zW_(Tfm8C-V6KH-Qd||0|hVm~s>?7Vm?8cMTqi&yjn8Vdxb|{m5;3^;EBT26opSrQBesTs}8Y}ttS|jZd|i+jAEsmZqHqJ z#!c#hYlmrysdQk31hR6XH!DIXs(Rd`9Mi~+y1gUKRg51qqNa+eZe~nAu-O>VX0|hZ z93RBK0AuR$`#?>knejjbH#){oGG5wIi&cw}ve zTbmNrrkJKFrfhnm&Yc;*+%VhlSY7l;T@+WBB-AA_b!D`Cm5!+`F{uUbf+anRuxvy! zUd#xZK&-Kd5YSCf+#yP_gyG&_r@~a27J7_^)YbJC=GpLjygKcJAeC6@UD?+3X+y8B zge6mwsC*+&q@*_=Ehu20!y- z5~gQ=Iz>S;N~UO#Dls9|!C%1sw&We-!TwQ(Kg%(_C))d3`+NHsN2cj&Ki=Be&5U?` zT!8U=m?6K<;~jPkdwI@n=X~SLAEy^dtcrm$M!@L{@a~ZjhK+C@cW{Jfye#MAy^y{p z&OU4x=W(;6USCxJ5<2A@;P^mh#wO8fwkPGOxk6c)$$Idsuy}kLL?|o0*rtC`O(gA6 zNRw8Hi;i?Rr4W#X&yXlLh@|wK-|I`NPIh&6cf0y}d;9Sqv3ZCfcu1;XMVJpvc=@xq z7Zhe`QZnQjPD%+vmNM?&uaM$W1e#Y`K-E;$#R^Nov*(9!6wMy^Db--v6hiHOF!jwCsC zYla<5p@mSiEc#m9U``m!t2+}0+hfC?M}|Fd!`_5p@0@f_x|Wx}RJu?a+qoy6SDVPI z1>i!cvFK;2YpQ7NDit?c5=P5vO~P3H*jW3>SQ|I)OBnaf$-Z67*3NX#wJoILRc z^0$dR%hSTcoq9Sd1GLEypN$at#;C)gEW~TlLn3renQrsCbPI{@=^nwm@G!qvMi5RO z2m|^KtgRcS{O_1H48;WRlC5|r`}1kb5U&^hVl1M0gYYoh7=af;8SH^)E5-a<-vg2P z2`nL@0)8KZgFc_vHw*~!25bzECrQWagBOM6uu}w}sm9E>eGK>d*ob$?%QMy0DgL%G zrafj6dfmretuOU=^!B-WT8~HC9h?0W&|RnA1dGp2FIluluOyY7r!mbXsJ zvhtp2^@~|6;}3}G1`*^wQfI1kxd@xPTuDh_GJpjVBZd1J$G?Q8 z;H|Dz6WRt?qJTpB;8yhttu~^3Hx<*LfuGR3yQ0g8@^p)e$J$+ww7X(u&bYQAp>3GX fS(hs)pxunnXH({Nlo2J{sFHQGRak`J?x+6)F!iL! delta 691 zcmZ2p_d1DhIWI340}y;T9grm~Kao#@QEa1n5)-5A?O?K#Gdi zrZA>3<)}odFfpX6rmAGAO;(T=ueZ)-0n0joL_jPMNU@fLFhFb@sD6E*KKT?|WOZc> zYwVUWGBB(LVhD&*2ddIYu?O3sjw&~sA;lq;Z#KhR7KHfZ3(T_mnqWPSU_F`$Igp7! zJx(BTAl6E8Zed8(T%bLfQCDelfu_J@KNfjLm&ti7(v0q#yI4%*8CfUqS5}oshPnkz zaRNzZApZP_agxeY#@flxRHB$t*e9o{>N4_7{wOLkd7G*rBj@B@s?w7mtI7zoqT0{M z&{H>ANbND>%E{lMgTjI8m>!O(UMIPgx2p|_qY*y1;%E&l*@)<2}#x0X2v|ZUiHWW>toUEv{new['template_version']}, " f"skill v{old['skill_version']}->v{new['skill_version']}") print(f"Changelog updated: {cl}") diff --git a/scripts/common.py b/scripts/common.py index b2f5c20..68243ec 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -155,7 +155,8 @@ def init_versions(force=False): # ---------------------------------------------------------------- changelog (read) -CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description"] +# "By" = who made the change (the operator). See operator identity below. +CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description", "By"] def read_changelog(): @@ -168,9 +169,9 @@ def read_changelog(): for r in ws.iter_rows(min_row=2, values_only=True): if not r or all(c is None for c in r): continue - r = list(r) + [None] * (5 - len(r)) + r = list(r) + [None] * (len(CHANGELOG_HEADERS) - len(r)) # pad old 5-col rows out.append({"date": r[0], "typeid": r[1], "skill": r[2], - "template": r[3], "description": r[4]}) + "template": r[3], "description": r[4], "by": r[5]}) return out @@ -180,6 +181,61 @@ def changelog_for_typeid(typeid): return [r for r in read_changelog() if str(r["typeid"]) == str(typeid)] +# ---------------------------------------------------------------- operator identity + +# Where each person's "who am I" is stored, on their OWN machine. The skill asks once, writes +# it here, and reads it silently on every later run so nobody has to retype their name. Because +# it lives on each person's computer (not in the shared config), it stays per-person even though +# the Claude account and the Gitea token are shared. +IDENTITY_FILE = os.path.expanduser("~/.library-manager-id") + + +def parse_author(author): + """'Name ' or 'Name' -> (name, email). Returns (None, None) for empty input.""" + if not author: + return None, None + m = re.match(r"^\s*(.*?)\s*<(.*?)>\s*$", str(author)) + if m: + return m.group(1).strip() or None, m.group(2).strip() + return str(author).strip() or None, "" + + +def _config_operator(): + """(name, email) from the OPERATOR / OPERATOR_EMAIL lines of config/gitea.env — the + per-install default identity (e.g. the admin's install sets OPERATOR=admin).""" + cfg = os.path.join(SKILL_ROOT, "config", "gitea.env") + name = email = None + if os.path.exists(cfg): + for line in open(cfg, encoding="utf-8"): + line = line.strip() + if line.startswith("OPERATOR="): + name = line.split("=", 1)[1].strip() + elif line.startswith("OPERATOR_EMAIL="): + email = line.split("=", 1)[1].strip() + return (name or None), (email or "") + + +def resolve_operator(author_arg=None): + """Return (name, email) for the person running this, trying in order: + (1) an explicit --author, (2) the LM_AUTHOR_NAME/LM_AUTHOR_EMAIL env vars, (3) the + per-person identity file ~/.library-manager-id, (4) the per-install OPERATOR default in + config/gitea.env. (None, None) if none set — callers fall back to the generic bot.""" + name, email = parse_author(author_arg) + if name: + return name, email + name = os.environ.get("LM_AUTHOR_NAME") + if name: + return name, os.environ.get("LM_AUTHOR_EMAIL", "") + if os.path.exists(IDENTITY_FILE): + try: + d = json.load(open(IDENTITY_FILE, encoding="utf-8")) + if d.get("name"): + return d.get("name"), d.get("email", "") + except Exception: + pass + return _config_operator() + + if __name__ == "__main__": import sys if len(sys.argv) > 1 and sys.argv[1] == "init": diff --git a/scripts/fill_templates.py b/scripts/fill_templates.py index a797194..c17973b 100644 --- a/scripts/fill_templates.py +++ b/scripts/fill_templates.py @@ -53,7 +53,7 @@ def add_version_history(wb, typeid): ws = wb.create_sheet(VERSION_SHEET) s = Side(style="thin", color=GRAY) border = Border(left=s, right=s, top=s, bottom=s) - headers = ["Date", "Skill Version", "Template Version", "Description"] + headers = ["Date", "Skill Version", "Template Version", "Description", "By"] for c, h in enumerate(headers, start=1): cell = ws.cell(1, c, h) cell.font = Font(name="Calibri", bold=True) @@ -62,12 +62,13 @@ def add_version_history(wb, typeid): cell.alignment = Alignment(horizontal="center", vertical="center") for row in rows: r = ws.max_row + 1 - vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]), row["description"]] + vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]), + row["description"], row.get("by") or ""] for c, v in enumerate(vals, start=1): cell = ws.cell(r, c, v) cell.border = border cell.alignment = Alignment(vertical="center", wrap_text=(c == 4), horizontal="left") - for col, w in zip("ABCD", (18, 16, 18, 60)): + for col, w in zip("ABCDE", (18, 16, 18, 50, 22)): ws.column_dimensions[col].width = w ws.freeze_panes = "A2" return True diff --git a/scripts/gitea_components.py b/scripts/gitea_components.py index 39871cc..0027631 100644 --- a/scripts/gitea_components.py +++ b/scripts/gitea_components.py @@ -44,7 +44,8 @@ nothing is written. import argparse, os, re, shutil, subprocess, sys, tempfile sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from common import (class_folder, load_taxonomy, mpn_make_prefix, - SKILL_ROOT, TEMPLATE_XLSX, VERSIONS_JSON, CHANGELOG_XLSX) + SKILL_ROOT, TEMPLATE_XLSX, VERSIONS_JSON, CHANGELOG_XLSX, + CHANGELOG_HEADERS, resolve_operator) CFG_DEFAULT = os.path.join(os.path.dirname(__file__), "..", "config", "gitea.env") @@ -89,9 +90,15 @@ def clone(env, dest, repo=None): return dest -def commit_push(root, token, msg): - for args in (["config", "user.email", "datasheet-bot@local"], - ["config", "user.name", "library-manager"], +def commit_push(root, token, msg, author=None): + """Commit + push. `author` = (name, email) of the operator; when given, the commit is + authored by that real person (visible in git log / Gitea commit view / blame). Without it, + a generic bot identity is used.""" + name, email = author or (None, None) + name = name or "library-manager" + email = email or "datasheet-bot@local" + for args in (["config", "user.email", email], + ["config", "user.name", name], ["checkout", "-B", "main"], ["add", "-A"]): subprocess.run(["git", "-C", root] + args, capture_output=True) if subprocess.run(["git", "-C", root, "diff", "--cached", "--quiet"]).returncode == 0: @@ -193,30 +200,50 @@ def cmd_place_part(env, args): print(f"placed {tag} -> {cls}/{tag}") +def _operator(args): + """(name, email) of who's running this — from --author, else env, else identity file.""" + return resolve_operator(getattr(args, "author", None)) + + +def _by(name): + return f" (by {name})" if name else "" + + def cmd_commit_push(env, args): - commit_push(args.root, env.get("GIT_TOKEN", ""), args.message or "library-manager: update components") + name, email = _operator(args) + commit_push(args.root, env.get("GIT_TOKEN", ""), + args.message or f"library-manager: update components{_by(name)}", author=(name, email)) def cmd_push_part(env, args): + name, email = _operator(args) tmp = tempfile.mkdtemp(prefix="components_") clone(env, tmp) cls, tag, _ = place_part(tmp, args.folder, args.typeid) - print(f"placed {tag} -> {cls}/{tag}") - commit_push(tmp, env.get("GIT_TOKEN", ""), args.message or f"library-manager: add {tag}") + print(f"placed {tag} -> {cls}/{tag}" + (f" (by {name})" if name else "")) + commit_push(tmp, env.get("GIT_TOKEN", ""), + args.message or f"library-manager: add {tag}{_by(name)}", author=(name, email)) # ------------------------------------------------------------------ skill repo + changelog def _changelog_rows(path): - """(workbook, sheet, [row-tuples]) for a changelog xlsx; ([]) if the file is absent.""" + """(workbook, sheet, [row-tuples]) for a changelog xlsx; ([]) if the file is absent. Rows are + padded/truncated to the canonical column count so old 5-column changelogs (no 'By') merge + cleanly with new 6-column ones.""" import openpyxl if not os.path.exists(path): return None, None, [] + n = len(CHANGELOG_HEADERS) wb = openpyxl.load_workbook(path) ws = wb["Changelog"] if "Changelog" in wb.sheetnames else wb.active - rows = [tuple("" if c is None else str(c) for c in r) - for r in ws.iter_rows(min_row=2, values_only=True)] + rows = [] + for r in ws.iter_rows(min_row=2, values_only=True): + vals = ["" if c is None else str(c) for c in r] + vals = (vals + [""] * n)[:n] + if any(v for v in vals): + rows.append(tuple(vals)) return wb, ws, rows @@ -234,6 +261,7 @@ def merge_changelog(local_path, repo_path): shutil.copy(local_path, repo_path) _, _, rows = _changelog_rows(repo_path) return len(rows) + from openpyxl.styles import Font, PatternFill repo_wb, repo_ws, repo_rows = _changelog_rows(repo_path) _, _, local_rows = _changelog_rows(local_path) seen = set(repo_rows) @@ -245,16 +273,26 @@ def merge_changelog(local_path, repo_path): seen.add(row) added += 1 merged.sort(key=lambda r: r[0]) # chronological by Date (column A) - if repo_ws.max_row > 1: # rewrite data rows, keep the header - repo_ws.delete_rows(2, repo_ws.max_row - 1) + n = len(CHANGELOG_HEADERS) + desc_col = CHANGELOG_HEADERS.index("Description") + 1 s = Side(style="thin", color="BFBFBF") border = Border(left=s, right=s, top=s, bottom=s) + if repo_ws.max_row > 1: # clear everything, rewrite header + rows + repo_ws.delete_rows(1, repo_ws.max_row) + for c, h in enumerate(CHANGELOG_HEADERS, start=1): # canonical header (migrates 5->6 cols) + cell = repo_ws.cell(1, c, h) + cell.font = Font(name="Calibri", bold=True) + cell.fill = PatternFill("solid", fgColor="B6D7A8") + cell.border = border + cell.alignment = Alignment(horizontal="center", vertical="center") for row in merged: - repo_ws.append(list(row)) - rr = repo_ws.max_row - for c in range(1, 6): - repo_ws.cell(rr, c).border = border - repo_ws.cell(rr, c).alignment = Alignment(vertical="center", wrap_text=(c == 5), horizontal="left") + rr = repo_ws.max_row + 1 + for c in range(1, n + 1): + cell = repo_ws.cell(rr, c, row[c - 1] if c - 1 < len(row) else "") + cell.border = border + cell.alignment = Alignment(vertical="center", wrap_text=(c == desc_col), horizontal="left") + repo_wb.freeze_panes = None + repo_ws.freeze_panes = "A2" repo_wb.save(repo_path) return added @@ -297,7 +335,10 @@ def cmd_push_skill(env, args): if os.path.exists(repo_cl): # sync merged history back to local shutil.copy(repo_cl, local_cl) print(f"changelog: merged (+{added} new row(s)) into the skill repo's CHANGELOG.xlsx") - commit_push(tmp, env.get("GIT_TOKEN", ""), args.message or "library-manager: sync skill files + changelog") + name, email = _operator(args) + commit_push(tmp, env.get("GIT_TOKEN", ""), + args.message or f"library-manager: sync skill files + changelog{_by(name)}", + author=(name, email)) def cmd_pull_skill(env, args): @@ -338,12 +379,14 @@ def main(): p.add_argument("--typeid", required=True); p.add_argument("--root"); p.add_argument("--local") p = sub.add_parser("commit-push"); p.add_argument("--root", required=True) - p.add_argument("--message") + p.add_argument("--message"); p.add_argument("--author", help="'Name ' of the operator") p = sub.add_parser("push-part"); p.add_argument("--folder", required=True) p.add_argument("--typeid", required=True); p.add_argument("--message") + p.add_argument("--author", help="'Name ' of the operator (who added the part)") p = sub.add_parser("push-skill") + p.add_argument("--author", help="'Name ' of the operator (who made the change)") p.add_argument("--src", default=os.path.join(os.path.dirname(__file__), ".."), help="skill directory to push (defaults to this skill)") p.add_argument("--message")