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-clean-schematic.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"
const read=p=>JSON.parse(fs.readFileSync(p,"utf8")),sha=b=>crypto.createHash("sha256").update(b).digest("hex"),hash=p=>sha(fs.readFileSync(p))
const packageName="shibosoftwaredev/f1c100s-linux-nema17-stepper-controller",version="0.4.55",action=process.argv[2]??"status"
const manifestPath="reports/clean-schematic-publish-manifest.json"
// Preserve small electrical quantities (notably pF/nF capacitors); an absolute
// geometry tolerance must never round those values to zero during comparison.
const stable=value=>JSON.stringify(value,(_,item)=>typeof item==="number"?(Math.abs(item)<1e-4?Number(item.toPrecision(12)):Math.round(item*1e8)/1e8):item&&typeof item==="object"&&!Array.isArray(item)?Object.fromEntries(Object.entries(item).sort(([a],[b])=>a.localeCompare(b))):item)
const records=c=>c.filter(e=>e.type!=="source_project_metadata").map(stable).sort()
async function request(endpoint,options={}){
const {body,write=false,missing=false,timeout=45000}=options
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(timeout)})
if(missing&&r.status===404)return null
assert.ok(r.ok,endpoint.split("?")[0]+" HTTP "+r.status);return r.json()
}
const pkg=()=>request("packages/get",{body:{name:packageName}}).then(r=>r.package)
const release=()=>request("package_releases/get",{body:{package_name_with_version:`${packageName}@${version}`},missing:true}).then(r=>r?.package_release??null)
const list=id=>request("package_files/list?"+new URLSearchParams({package_release_id:id})).then(r=>r.package_files)
async function bytes(entry){const e=(await request("package_files/get?"+new URLSearchParams({package_file_id:entry.package_file_id}))).package_file;return e.content_text!=null?Buffer.from(e.content_text):Buffer.from(e.content_base64,"base64")}
const walk=dir=>fs.readdirSync(dir,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(path.join(dir,e.name)):[path.join(dir,e.name)])
if(action==="prepare"){
const a=read("reports/clean-schematic-acceptance.json")
assert.equal(a.status,"schematic_only_regression_gates_passed");assert.equal(a.testsPassed,57);assert.equal(a.schematicStyleIssues,0)
assert.equal(hash("dist/index/circuit.json"),a.circuitSha256)
for(const [file,digest]of Object.entries(a.reports))assert.equal(hash(file),digest,file)
const previous=await pkg();assert.equal(previous.latest_version,"0.4.54");assert.equal(previous.latest_package_release_id,"985dd0d3-80fb-4044-a69d-0f7dea7dcf54");assert.equal(await release(),null,"Do not overwrite releases")
const baselineEntry=(await list(previous.latest_package_release_id)).find(e=>e.file_path==="dist/index/circuit.json")
assert.deepEqual(records(JSON.parse(await bytes(baselineEntry))),records(read("baseline/circuit.json")))
const selected=new Set(read("../nema17-bulk-top-swap-ejGH6E/reports/bulk-top-publish-manifest.json").files.map(e=>e.file))
for(const file of ["baseline/circuit.json","schematic-plan.ts","schematic-pages.ts","schematic-refinements.json","schematic-text-placements.json","schematic-guide.tsx","README.md","reports/clean-schematic-acceptance.json","reports/swap-preparation.json","reports/bulk-top-physical-islands.json",...Object.keys(a.reports),...walk("scripts"),...walk("tests"),...fs.readdirSync("reports").filter(f=>/^review-.*\.log$/.test(f)).map(f=>"reports/"+f)])selected.add(file)
const destination=fs.mkdtempSync(path.resolve("../../publish-staging/nema17-clean-schematic-v055-"))
for(const file of selected){
assert.ok(!file.includes("..")&&!/(?:^|\/)(?:node_modules|\.env|\.npmrc|\.netrc|__pycache__)(?:\/|$)/.test(file),file)
const b=fs.readFileSync(file);assert.ok(!b.includes(0)&&Buffer.from(b.toString("utf8")).equals(b),file)
const target=path.join(destination,file);fs.mkdirSync(path.dirname(target),{recursive:true});fs.copyFileSync(file,target)
}
const files=[...selected].sort().map(file=>({file,sha256:hash(path.join(destination,file)),bytes:fs.statSync(path.join(destination,file)).size}))
const manifest={createdAt:new Date().toISOString(),packageName,version,destination,previous:{version:previous.latest_version,releaseId:previous.latest_package_release_id},circuitSha256:a.circuitSha256,files}
fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+"\n");console.log(JSON.stringify({version,destination,files:files.length,bytes:files.reduce((t,f)=>t+f.bytes,0)}))
}else{
const m=read(manifestPath);let r=await release(),p=await pkg()
assert.ok([m.previous.releaseId,r?.package_release_id].includes(p.latest_package_release_id),"Unrelated latest update: stop")
if(action==="recover-upload"||action==="hold"){
if(p.latest_package_release_id!==m.previous.releaseId)await request("package_releases/update",{write:true,body:{package_release_id:m.previous.releaseId,is_latest:true}})
}
if(action==="recover-upload"){
assert.ok(r,"Resume only the release created by tsci push")
const files=m.files.map(f=>{const b=fs.readFileSync(path.join(m.destination,f.file));assert.equal(sha(b),f.sha256);return{file_path:f.file,content_text:b.toString("utf8")}})
const archive=gzipSync(JSON.stringify({files}),{level:9});console.log(JSON.stringify({uploading:true,releaseId:r.package_release_id,gzipBytes:archive.length}))
await request("package_files/upload_archive",{write:true,timeout:240000,body:{package_name_with_version:`${packageName}@${version}`,archive_base64:archive.toString("base64")}})
await request("package_releases/update",{write:true,body:{package_release_id:r.package_release_id,ready_to_build:true}})
}
if(action==="verify"||action==="promote"){
assert.ok(r)
const entries=new Map((await list(r.package_release_id)).map(e=>[e.file_path,e])),pending=[...m.files]
await Promise.all(Array.from({length:6},async()=>{while(pending.length){const f=pending.shift();assert.ok(entries.has(f.file),f.file);assert.equal(sha(await bytes(entries.get(f.file))),f.sha256,f.file)}}))
r=await release();const b=(await request("package_builds/get?"+new URLSearchParams({package_build_id:r.latest_package_build_id}))).package_build
assert.ok(b.user_code_job_completed_at&&!b.build_in_progress&&!b.user_code_job_error&&!b.circuit_json_build_error&&!b.transpilation_error&&!b.image_generation_error&&!b.user_code_job_infrastructure_error,"Successful completed website build required")
const outputs=new Map((await list(r.package_release_id)).map(e=>[e.file_path,e]))
for(const file of ["dist/index/circuit.json","dist/index/pcb.svg","dist/index/schematic.svg","dist/index/3d.png","dist/index.html"])assert.ok(outputs.has(file),file)
const cloudBytes=await bytes(outputs.get("dist/index/circuit.json")),cloud=JSON.parse(cloudBytes)
assert.equal(hash("dist/index/circuit.json"),m.circuitSha256);assert.deepEqual(records(cloud),records(read("dist/index/circuit.json")),"Cloud-generated circuit must match audited local circuit")
assert.equal(cloud.filter(e=>e.type.endsWith("_error")).length,0);assert.equal(cloud.filter(e=>e.type.endsWith("_warning")).length,151)
fs.writeFileSync("reports/clean-schematic-website-circuit.json",cloudBytes)
if(action==="promote")await request("package_releases/update",{write:true,body:{package_release_id:r.package_release_id,is_latest:true}})
p=await pkg();if(action==="promote")assert.equal(p.latest_package_release_id,r.package_release_id)
const report={checkedAt:new Date().toISOString(),version,releaseId:r.package_release_id,buildId:r.latest_package_build_id,buildCompletedAt:b.user_code_job_completed_at,sourceFilesVerified:m.files.length,cloudMatchesAuditedBoard:true,localCircuitSha256:m.circuitSha256,cloudCircuitSha256:sha(cloudBytes),latestVerified:p.latest_package_release_id===r.package_release_id,currentLatest:p.latest_version,fabricationReady:false}
fs.writeFileSync("reports/clean-schematic-publication.json",JSON.stringify(report,null,2)+"\n");console.log(JSON.stringify(report))
}else{
r=await release();p=await pkg();const b=r?.latest_package_build_id?(await request("package_builds/get?"+new URLSearchParams({package_build_id:r.latest_package_build_id}))).package_build:null
if(b)fs.writeFileSync("reports/clean-cloud-build-status.json",JSON.stringify(b,null,2)+"\n")
console.log(JSON.stringify({version,releaseId:r?.package_release_id,latestVersion:p.latest_version,buildId:r?.latest_package_build_id,inProgress:b?.build_in_progress,completedAt:b?.user_code_job_completed_at,errors:b?[b.user_code_job_error,b.circuit_json_build_error,b.transpilation_error,b.image_generation_error,b.user_code_job_infrastructure_error].filter(Boolean):[]}))
}
}