release/: JLCPCB order files when the repository makes them

A repository with .gitea/scripts/jlcpcb-files.py (harNES) gets
<board>-<tag>-jlcpcb-bom.csv and -jlcpcb-cpl.csv (rotations corrected per
LCSC part) in release/: the script runs with --force on a copy of hardware/
without the committed jlcpcb/ folders, so nothing stale is taken. When it
refuses a board (DRC, missing correction), release/README.md and a workflow
warning say why; the generic -cpl.csv then is the only placement file.
This commit is contained in:
grabowski committed 2026-10-16 19:01:20 +07:00
1 parent 52a40c1c3e
commit 59f3df278b
2 files changed
+61 -5

No files matched your search

+57 -3
View File
@@ -13,6 +13,8 @@ Writes into --out (default release/; existing content is replaced):
<board>-<version>-pcb.pdf one page per copper, silkscreen and fab layer
<board>-<version>-top.png 3D render, top
<board>-<version>-bottom.png 3D render, bottom
<board>-<version>-jlcpcb-bom.csv, -jlcpcb-cpl.csv JLCPCB order files with rotations
corrected, when the repository has .gitea/scripts/jlcpcb-files.py (harNES)
docs/<name>-<version>.pdf docs/*.md (pandoc, pdflatex) and docs/tex/*.tex (pdflatex)
Boards are the [[boards]] of proplan.toml, or hardware/*/*.kicad_pro when it names none. DRC and ERC
@@ -190,7 +192,42 @@ def cpl(pcb, dest):
return f"pick and place: {max(len(rows) - 1, 0)} parts, both sides, mm, DNP left out"
def board(board_dir, pro, version, out, tmp):
def jlcpcb_files(tmp):
"""The repository's own JLCPCB order files, if it has .gitea/scripts/jlcpcb-files.py (harNES's pattern:
JLCPCB BOM, CPL with rotations corrected per LCSC part, refused unless DRC is clean). It runs with
--force on a copy of hardware/ without the committed jlcpcb/ folders, so nothing stale is taken.
Returns {board folder relative to the root: (jlcpcb folder or None, why not)}."""
script = ROOT / ".gitea" / "scripts" / "jlcpcb-files.py"
if not script.exists():
return {}
work = tmp / "jlcpcb-work"
shutil.copytree(ROOT / "hardware", work / "hardware", ignore=shutil.ignore_patterns("jlcpcb", "render", "*-backups"))
(work / ".gitea" / "scripts").mkdir(parents=True)
shutil.copyfile(script, work / ".gitea" / "scripts" / script.name)
r = run([sys.executable, work / ".gitea" / "scripts" / script.name, "--force"], cwd=work, check=False)
print(" " + "\n ".join((r.stdout + r.stderr).strip().splitlines()))
blocks = {b.split("\n", 1)[0].split(":")[0].strip(): b for b in re.split(r"^== ", r.stdout, flags=re.M) if b.strip()}
result = {}
for pro in sorted(work.glob("hardware/*/*.kicad_pro")):
folder = pro.parent / "jlcpcb"
rel = pro.parent.relative_to(work).as_posix()
if (folder / f"{pro.stem}-bom.csv").exists() and (folder / f"{pro.stem}-cpl.csv").exists():
result[rel] = (folder, "")
else:
lines = [l.strip() for l in blocks.get(pro.stem, "").splitlines()[1:] if l.strip()]
i = next((k for k, l in enumerate(lines) if l.startswith("NOT written")), None)
if i is None: # no verdict: the script crashed (its last line says why)
why = (r.stderr.strip().splitlines() or ["jlcpcb-files.py failed"])[-1]
else: # "NOT written: DRC is not clean" or "NOT written:", then one line per problem
head, rest = lines[i].partition(":")[2].strip(), lines[i + 1:]
why = "; ".join(x for x in (head, rest[0] if rest else "") if x) or "NOT written"
if len(rest) > 1:
why += f" (+{len(rest) - 1} more)"
result[rel] = (None, why[:300])
return result
def board(board_dir, pro, version, out, tmp, jlc):
"""Write one board's files; returns (name, [(file, what)], checks)."""
name = pro.stem
sch, pcb = board_dir / f"{name}.kicad_sch", board_dir / f"{name}.kicad_pcb"
@@ -210,6 +247,17 @@ def board(board_dir, pro, version, out, tmp):
f"{d['schematic_parity']} schematic parity issues")
files.insert(0, (f"{base}-gerbers.zip", gerbers_zip(pcb, dest / f"{base}-gerbers.zip", tmp)))
files.append((f"{base}-cpl.csv", cpl(pcb, dest / f"{base}-cpl.csv")))
rel = board_dir.relative_to(ROOT).as_posix()
if rel in jlc:
folder, why = jlc[rel]
if folder:
for kind, what in (("bom", "JLCPCB BOM: one line per LCSC part number"),
("cpl", "JLCPCB CPL: rotations and centres corrected for JLCPCB's footprints")):
shutil.copyfile(folder / f"{name}-{kind}.csv", dest / f"{base}-jlcpcb-{kind}.csv")
files.append((f"{base}-jlcpcb-{kind}.csv", what))
files[-3] = (files[-3][0], files[-3][1] + "; KiCad rotations, for JLCPCB use the -jlcpcb-cpl.csv")
else:
checks.append(f"JLCPCB files not made ({why})")
copper = ",".join(copper_layers(pcb))
cli("pcb", "export", "pdf", "--mode-multipage", "--include-border-title",
"--layers", f"{copper},F.Silkscreen,B.Silkscreen,F.Fab,B.Fab", "--common-layers", "Edge.Cuts",
@@ -358,6 +406,11 @@ def main():
kicad = cli("version").strip()
clear(out)
boards, skipped, ok = [], [], True
jlc_tmp = tempfile.TemporaryDirectory()
jlc = {}
if (ROOT / ".gitea" / "scripts" / "jlcpcb-files.py").exists():
print("== JLCPCB order files (.gitea/scripts/jlcpcb-files.py)")
jlc = jlcpcb_files(Path(jlc_tmp.name))
for board_dir, pro in projects(m):
rel = board_dir.relative_to(ROOT).as_posix()
if pro is None:
@@ -367,7 +420,7 @@ def main():
print(f"== {pro.stem}")
with tempfile.TemporaryDirectory() as t:
try:
name, files, checks = board(board_dir, pro, a.version, out, Path(t))
name, files, checks = board(board_dir, pro, a.version, out, Path(t), jlc)
except Failed as e:
print(f"::error::{pro.stem}: {e}")
ok = False
@@ -375,10 +428,11 @@ def main():
boards.append((name, files, checks))
for c in checks:
print(f" {c}")
if re.search(r"[1-9]\d* (errors|unconnected|schematic)", c):
if re.search(r"[1-9]\d* (errors|unconnected|schematic)", c) or c.startswith("JLCPCB"):
print(f"::warning::{name} {c}")
for f, _ in files:
print(f" hardware/{name}/{f}")
jlc_tmp.cleanup()
print("== documentation")
foot = tex_escape(f"{product} {a.version}, {commit}, {when[:10]}")
docs_made, docs_failed = documents(a.version, foot, out)