shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/publish-round5.mjs
import fs from "node:fs"
import path from "node:path"
import os from "node:os"
import crypto from "node:crypto"
import assert from "node:assert/strict"
import { gzipSync } from "node:zlib"
// Same registry archive workflow as tsci push, with a bounded longer upload
// timeout. No retry-created versions, and no latest promotion before checks.
const mode=process.argv[2],round=process.env.PUBLICATION_ROUND??"round5"
assert.ok(["round5","round10","round12"].includes(round))
const manifestPath=`reports/${round}-publish-manifest.json`
const read=p=>JSON.parse(fs.readFileSync(p,"utf8"))
const sha=b=>crypto.createHash("sha256").update(b).digest("hex")
async function request(endpoint,{body,write=false,allowMissing=false}={}) {
const token=write?(process.env.TSCIRCUIT_TOKEN??read(path.join(os.homedir(),"Library/Preferences/tscircuit-nodejs/config.json")).sessionToken):null
const r=await fetch("https://registry-api.tscircuit.com/"+endpoint,{method:body?"POST":"GET",
headers:{...(body?{"content-type":"application/json"}:{}),...(token?{authorization:`Bearer ${token}`}:{})},
body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(endpoint==="package_files/upload_archive"?240000:45000)})
if(allowMissing&&r.status===404)return null
assert.ok(r.ok,`${endpoint.split("?")[0]} HTTP ${r.status}`);return r.json()
}
if(mode==="--prepare"&&round==="round12") {
await import("./prepare-round12-publication.mjs")
process.exit(0)
}
if(mode==="--prepare"&&round==="round10") {
const status=read("reports/round10-fix-status.json")
assert.equal(sha(fs.readFileSync("dist/index/circuit.json")),status.circuitSha256)
for(const [p,h] of Object.entries(status.evidence))assert.equal(sha(fs.readFileSync(p)),h,`fresh validated evidence ${p}`)
assert.equal(status.checks.nativeErrors,0);assert.equal(status.realWidthRegressionFixed,true)
assert.equal(status.invariants.ordinaryPadViaConflicts,0);assert.equal(status.fabricationReady,false)
const packageName="shibosoftwaredev/f1c100s-linux-nema17-stepper-controller",version="0.4.50-dev-20261020"
const pkg=(await request("packages/get",{body:{name:packageName}})).package
const previous={latestReleaseId:pkg.latest_package_release_id,latestVersion:pkg.latest_version}
assert.equal(previous.latestVersion,"0.4.49-review-fixes-20261019","stop for an unrelated concurrent latest")
assert.equal(await request("package_releases/get",{body:{package_name_with_version:`${packageName}@${version}`},allowMissing:true}),null)
const root=path.resolve("../../publish-staging");fs.mkdirSync(root,{recursive:true})
const destination=fs.mkdtempSync(path.join(root,"nema17-round10-"))
const selected=new Set(fs.readdirSync(".").filter(f=>fs.statSync(f).isFile()&&!f.startsWith(".")&&/\.(tsx?|json|md)$/.test(f)))
selected.add("bun.lock");selected.add("requirements-routing.txt")
function visit(dir){for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(["build","__pycache__","node_modules",".git"].includes(e.name))continue;if(e.isDirectory())visit(p);else if(e.isFile()&&!e.name.startsWith(".env"))selected.add(p)}}
for(const dir of ["imports","footprints","modules","firmware","tests","scripts"])visit(dir)
for(const p of ["routing/complete-routes.json","routing/expected-input.json","routing/placement.json","routing/native-features.json",
"routing/net-map.json","routing/trace-constraints.json","routing/pad-metadata-normalizations.json","routing/physical-copper.json",
"routing/import-report.json","routing/emitted-seed.ses","reports/round10-fix-status.json","reports/round10-width-proof.json",
...Object.keys(status.evidence),"dist/index/circuit.json","dist/index/pcb.svg","dist/index/schematic.svg"])selected.add(p)
for(const file of [...selected].sort()){
assert.ok(!/(?:^|\/)(?:\.env|\.npmrc|\.netrc|\.tscircuit|node_modules)(?:\/|$)/.test(file))
const target=path.join(destination,file);fs.mkdirSync(path.dirname(target),{recursive:true});fs.copyFileSync(file,target)
}
const packageJson=read("package.json")
packageJson.version="0.4.50"
packageJson.description="DEVELOPMENT / NOT FABRICATION APPROVED. "+packageJson.description
fs.writeFileSync(path.join(destination,"package.json"),JSON.stringify(packageJson,null,2)+"\n")
const lock=read("package-lock.json");lock.version="0.4.50";if(lock.packages?.[""])lock.packages[""].version="0.4.50"
fs.writeFileSync(path.join(destination,"package-lock.json"),JSON.stringify(lock,null,2)+"\n")
const notice=`# Development release ${version}\n\nPublished from the restore-verified round-10 board. Not fabrication approved.\nStock shortages, thermal/double-sided assembly, actual rear mounting and bench\nqualification remain. Reports preserve their pre-publication timestamps/status;\nthe registry release and publication report establish the current published state.\n\n`
for(const p of ["README.md","CHECKPOINT_SAVED.md"])fs.writeFileSync(path.join(destination,p),notice+fs.readFileSync(p,"utf8"))
fs.writeFileSync(path.join(destination,"verification.json"),JSON.stringify({circuitSha256:status.circuitSha256,
fabricationReady:false,fullVerificationPassed:false,nativeErrors:0,nativeWarnings:153,
blocker:"W5500/BSS84 orderable stock; thermal/assembly, actual rear mounting and hardware qualification remain",
report:"reports/round10-fix-status.json",developmentPublicationOnly:true},null,2)+"\n")
const files=[...selected].sort().map(file=>{const bytes=fs.readFileSync(path.join(destination,file));return {file,sha256:sha(bytes),bytes:bytes.length}})
const manifest={createdAt:new Date().toISOString(),source:process.cwd(),destination,packageName,version,previous,
circuitSha256:status.circuitSha256,fabricationReady:false,files,
stagingOnlyMetadataChanges:["package.json","package-lock.json","README.md","CHECKPOINT_SAVED.md","verification.json"]}
fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+"\n")
console.log(JSON.stringify({version,destination,files:files.length,totalBytes:files.reduce((s,f)=>s+f.bytes,0),previous}))
process.exit(0)
}
if(mode==="--prepare") {
const status=read("reports/round5-fix-status.json"),old=read("reports/round3-publish-manifest.json")
assert.equal(sha(fs.readFileSync("dist/index/circuit.json")),status.circuitSha256)
assert.equal(status.checks.nativeErrors,0);assert.equal(status.fabricationReady,false)
const version="0.4.49-review-fixes-20261019"
assert.equal(read("package.json").version,"0.4.49")
const pkg=(await request("packages/get",{body:{name:old.packageName}})).package
const previous={latestReleaseId:pkg.latest_package_release_id,latestVersion:pkg.latest_version}
assert.equal(previous.latestVersion,"0.4.47-review-fixes-20261019","stop for unrelated concurrent latest changes")
assert.equal(await request("package_releases/get",{body:{package_name_with_version:`${old.packageName}@${version}`},allowMissing:true}),null,"new publication version must not already exist")
const root=path.resolve("../../publish-staging");fs.mkdirSync(root,{recursive:true})
const destination=fs.mkdtempSync(path.join(root,"nema17-round5-"))
const added=["manufacturing-legend.ts","scripts/audit-solder-process.py","scripts/check-round5-regression.mjs",
"scripts/plan-manufacturing-legend.py","scripts/render-round5-pcb.mjs","scripts/select-round5-vias.mjs",
"scripts/widen-5v-candidate.py","scripts/write-round5-status.mjs","scripts/test-round3-copper-gate.mjs",
"scripts/compare-round3-module-tests.mjs","scripts/test-export-gates.mjs",
"reports/solder-process-audit.json","reports/round5-fix-status.json","reports/round5-regression.json",
"reports/round5-legend-audit.json","reports/round5-selected-vias.json","reports/round5-5v-width-plan.json",
...Object.keys(status.evidence)]
const files=[...new Set([...old.files.map(f=>f.file),...added])].sort().map(file=>{
const bytes=fs.readFileSync(file),target=path.join(destination,file)
fs.mkdirSync(path.dirname(target),{recursive:true});fs.copyFileSync(file,target)
return {file,sha256:sha(bytes),bytes:bytes.length}
})
const manifest={createdAt:new Date().toISOString(),source:process.cwd(),destination,packageName:old.packageName,
version,previous,circuitSha256:status.circuitSha256,fabricationReady:false,files}
fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+"\n")
console.log(JSON.stringify({version,destination,files:files.length,totalBytes:files.reduce((s,f)=>s+f.bytes,0),previous}))
process.exit(0)
}
const m=read(manifestPath),nameWithVersion=`${m.packageName}@${m.version}`
let release=(await request("package_releases/get",{body:{package_name_with_version:nameWithVersion},allowMissing:true}))?.package_release
let pkg=(await request("packages/get",{body:{name:m.packageName}})).package
assert.ok([m.previous.latestReleaseId,release?.package_release_id].includes(pkg.latest_package_release_id),"stop for concurrent unrelated update")
if(mode==="--upload") {
if(!release) {
await request("package_releases/create",{body:{package_name_with_version:nameWithVersion},write:true})
release=(await request("package_releases/get",{body:{package_name_with_version:nameWithVersion}})).package_release
}
pkg=(await request("packages/get",{body:{name:m.packageName}})).package
if(pkg.latest_package_release_id!==m.previous.latestReleaseId)
await request("package_releases/update",{body:{package_release_id:m.previous.latestReleaseId,is_latest:true},write:true})
const payload=m.files.map(f=>{
const b=fs.readFileSync(path.join(m.destination,f.file));assert.equal(sha(b),f.sha256)
const binary=b.subarray(0,8192).includes(0)||/\.(png|jpe?g|webp|glb|zip)$/i.test(f.file)
return binary?{file_path:f.file,content_base64:b.toString("base64")}:{file_path:f.file,content_text:b.toString("utf8")}
})
// Keep the JSON/base64 request comfortably below the gateway body limit.
// Each archive upserts only its named files; final byte verification is
// mandatory before ready_to_build is set.
const batches=[]
function split(files) {
const archive=gzipSync(JSON.stringify({files}),{level:9})
if(archive.length>2_000_000) {
assert.ok(files.length>1,`single file exceeds upload batch limit: ${files[0].file_path}`)
const half=Math.ceil(files.length/2);split(files.slice(0,half));split(files.slice(half))
} else batches.push({files,archive})
}
split(payload)
for(const [index,{files,archive}] of batches.entries()) {
console.log(JSON.stringify({uploading:true,releaseId:release.package_release_id,batch:index+1,batches:batches.length,files:files.length,compressedBytes:archive.length}))
await request("package_files/upload_archive",{body:{package_name_with_version:nameWithVersion,archive_base64:archive.toString("base64")},write:true})
}
console.log(JSON.stringify({uploaded:true,releaseId:release.package_release_id}))
} else if(!["--verify","--ready","--promote","--status"].includes(mode))throw Error("unknown publication action")
assert.ok(release,"publication release exists")
const entries=(await request("package_files/list?"+new URLSearchParams({package_release_id:release.package_release_id}))).package_files
async function remoteFile(filename) {
const entry=entries.find(f=>f.file_path===filename);assert.ok(entry,`uploaded ${filename}`)
const f=(await request("package_files/get?"+new URLSearchParams({package_file_id:entry.package_file_id}))).package_file
if(f.is_text===false&&f.content_base64==null) {
const r=await fetch("https://registry-api.tscircuit.com/package_files/download?"+new URLSearchParams({package_file_id:entry.package_file_id}),{signal:AbortSignal.timeout(45000)})
assert.ok(r.ok);return Buffer.from(await r.arrayBuffer())
}
return f.content_text!=null?Buffer.from(f.content_text):Buffer.from(f.content_base64??"","base64")
}
const report={checkedAt:new Date().toISOString(),version:m.version,releaseId:release.package_release_id,
url:`https://tscircuit.com/${m.packageName}?version=${m.version}#3d`,files:entries.length,
auditedCircuitSha256:m.circuitSha256,fabricationReady:false,previousVersion:m.previous.latestVersion}
if(["--verify","--ready","--promote"].includes(mode)) {
const pending=m.files.filter(f=>!f.file.startsWith("dist/"));let checked=0
await Promise.all(Array.from({length:4},async()=>{while(pending.length){const f=pending.shift();assert.equal(sha(await remoteFile(f.file)),f.sha256,`exact uploaded source ${f.file}`);checked++}}))
report.sourceFilesVerified=checked
}
if(mode==="--ready")await request("package_releases/update",{body:{package_release_id:release.package_release_id,ready_to_build:true},write:true})
release=(await request("package_releases/get",{body:{package_name_with_version:nameWithVersion}})).package_release
report.buildId=release.latest_package_build_id;report.readyToBuild=release.ready_to_build
if(report.buildId) {
const build=(await request("package_builds/get?"+new URLSearchParams({package_build_id:report.buildId}))).package_build
report.buildError=build.user_code_job_error??build.circuit_json_build_error??build.transpilation_error??null
report.buildCompletedAt=build.user_code_job_completed_at
report.buildInProgress=build.user_code_job_started_at&&!build.user_code_job_completed_at&&!report.buildError
}
if(report.buildCompletedAt&&!report.buildError) {
const bytes=await remoteFile("dist/index/circuit.json"),remote=JSON.parse(bytes),local=read(path.join(m.destination,"dist/index/circuit.json"))
const types=new Set(["pcb_board","pcb_component","pcb_port","pcb_smtpad","pcb_plated_hole","pcb_hole","pcb_trace","pcb_via","pcb_copper_pour","pcb_keepout","source_component","source_port","source_trace","source_net","source_component_internal_connection","pcb_silkscreen_text","pcb_silkscreen_circle","pcb_silkscreen_path","cad_component"])
const stable=x=>JSON.stringify(x,(_,v)=>typeof v==="number"?Math.round(v*1e8)/1e8:v&&typeof v==="object"&&!Array.isArray(v)?Object.fromEntries(Object.entries(v).sort(([a],[b])=>a.localeCompare(b))):v)
const critical=c=>c.filter(e=>types.has(e.type)||e.type.startsWith("schematic_")||e.type.startsWith("cad_")).map(stable).sort()
const remoteCritical=critical(remote),localCritical=critical(local)
const differences=[...new Set([...remoteCritical,...localCritical])].filter(e=>remoteCritical.includes(e)!==localCritical.includes(e))
if(differences.length)fs.writeFileSync(`reports/${round}-cloud-differences.json`,JSON.stringify(differences.map(e=>JSON.parse(e)),null,2)+"\n")
assert.equal(sha(Buffer.from(JSON.stringify(remoteCritical))),sha(Buffer.from(JSON.stringify(localCritical))),"cloud electrical/PCB/schematic/legend/CAD matches frozen audited board")
assert.equal(remote.filter(e=>e.type.endsWith("_error")).length,0)
report.cloudMatchesAuditedBoard=true;report.cloudCircuitSha256=sha(bytes)
report.nativeErrors=0;report.nativeWarnings=remote.filter(e=>e.type.endsWith("_warning")).length
fs.writeFileSync(`reports/${round}-website-circuit.json`,bytes)
if(mode==="--promote") {
pkg=(await request("packages/get",{body:{name:m.packageName}})).package
assert.ok([m.previous.latestReleaseId,release.package_release_id].includes(pkg.latest_package_release_id))
if(pkg.latest_package_release_id!==release.package_release_id)
await request("package_releases/update",{body:{package_release_id:release.package_release_id,is_latest:true},write:true})
}
} else if(mode==="--promote")throw Error("Do not promote an incomplete/failed cloud build")
pkg=(await request("packages/get",{body:{name:m.packageName}})).package
report.latestVersion=pkg.latest_version;report.latestVerified=pkg.latest_package_release_id===release.package_release_id
fs.writeFileSync(`reports/${round}-publication.json`,JSON.stringify(report,null,2)+"\n")
console.log(JSON.stringify(report))