Releases: wighawag/pinnace
Release list
pinnace@0.18.0
Minor Changes
-
125026c: Add
pinnace set <id>— change a live site's metadata (mode, ensName, keep) without rebuilding or re-placing its contentA site already deployed in
ipfsmode can now be promoted toipns(or get anensName, or a retention policy) in one command, using the content CID the nodes already hold:pinnace set --set-mode ipns --set-ens-name mysite.eth mysiteIt reads the site's current content CID from MFS, writes the resolved
metadata.json, applies the site'skeeppolicy, and inipnsmode publishes the IPNS record pointing at that existing CID. No CAR is built and no content is imported or re-placed. The same mode/ensName/keep flags asdeployandpinapply, resolved the same way (stated > stored > default; omitting preserves).Because it places no content, it is stricter than
deployabout what it will infer: it REFUSES when the publisher does not hold the site, rather than resolving a preservedmodeto theipfsdefault and writing that demotion to the nodes that do hold it. It also reports the publisher's CID (the one the name resolves to) and names any node holding a different build, sincesetcannot fix that.Also adds
readSiteContentCidForWrite/SiteContentUnreadableErrorto the library surface: the content-side twin of the existing strict metadata read, so a node that will not answer is never reported as a node that does not have the site.
pinnace@0.17.0
Minor Changes
-
26a79a6: Dashboard: copy-to-clipboard buttons on opaque identifiers + ENS field grouping.
- Each opaque identifier (content CID, IPNS) now has a one-tap copy button beside it. The page gains a tiny progressive-enhancement
<script>(the only client-side JS); without JS the values stay fully visible, selectable, and linked — nothing is lost. - The four ENS-specific fields (
ens name,eth.limo,origin,freshness) are now grouped under a small ENS subheading, separate from the core fields (content cid,ipns,sequence,mode,announced,gateway). - The ENS group is shown only when the site resolves an ENS name to warm (
ensNameToWarm). A site that opts out, resolves no name, or comes from a report path that does no resolving shows no ENS group — the four fields are all not-applicable, so showing them was noise without signal.
- Each opaque identifier (content CID, IPNS) now has a one-tap copy button beside it. The page gains a tiny progressive-enhancement
pinnace@0.16.0
Minor Changes
-
5bfa2f7: Redesign the
node statusdashboard as centered per-site cards with full, readable identifiers.The old page was one eleven-column table: the long nowrap cells pushed it past its centered column (so it read as off-centre), and the opaque
cid/ipnsidentifiers had to be middle-elided to fit their cramped cells — hard to read and to compare across nodes. Each site is now a card on its own, so:- the page is centered and never overflows its column;
- every opaque id (
cid,ipns, the nodepeerId, and theorigin/freshnesspaths) gets its own line at the card's full width and is shown in full (no elision), as an inverted-background badge that folds withoverflow-wrap: anywhereinstead of shredding into a one-character-per-line ribbon; - a light/dark color scheme keeps the inverted id badge legible in both modes.
The full values still survive in the link
href/titleand instatus.json, so showing them loses nothing. The honesty rules are unchanged: a check that could not run still renders the neutralunknown (<reason>), never the red negative, and an absent verdict still readsnone. -
a112f31: Print the publish recipe in the CI summary:
promote-to/install-ci --promote-to <id>.An
ipfs-mode staging pipeline ends with two steps no tool can take for you: promote the build to the live site, then move the ENS contenthash. They are easy to get wrong a week later, and the job summary is exactly where someone is already looking, so it now spells them out with the ids and the cid filled in:This is a staging build:
mandalas.ethstill serves whatever it served before.
To publish it, in this order:pnpm pinnace pin --from-site mandalas-staging --as mandalas.eth --endpoint https://ipfs-publisher.example.com
- set the ENS contenthash of
mandalas.ethtoipfs://bafy...
The composite action gains a
promote-toinput andinstall-cia--promote-to <id>flag that bakes it in. It changes no behaviour, only the summary. The order is stated too, because it matters: promoting first means the node is already serving and warming that cid when the record moves, and the previous build stays pinned throughout, so nothing goes dark in between.Refused up-front:
--promote-towith--set-mode ipns(an ipns deploy re-signs its own name, so there is nothing to promote), and a promotion target equal to the site being deployed. - set the ENS contenthash of
pinnace@0.15.0
Minor Changes
-
c5576d9: Add
pin --from-site <id>: promote a staging site's current build to the live site.A site wrapper means "the cid this name resolves to". In
ipfsmode, where every build has its own address and the ENS record only moves when a human moves it, a pipeline that deploys straight to the LIVE id therefore breaks that meaning on every push: the box warms a cid nobody resolves,statusreportsfreshness=stalefor ever, and the cid the record actually points at becomes UNTRACKED, which would let any future retention policy reclaim the live build.The fix is a third pin source. CI deploys to a staging id, and a human promotes when they are happy:
pinnace deploy ./web/build mandalas-staging # CI, every push pinnace pin --from-site mandalas-staging --as mandalas.eth # you, when ready
- The source cid is read from MFS on ONE node and that single cid is fanned out, so an unevenly-landed deploy can never promote two different builds to two boxes. The publisher is read first (its view is the authoritative one, since it is the node that signs names), with the other targets as a reachability fallback so a down publisher cannot block a promotion. The node that answered is reported.
- The destination keeps its OWN metadata: an omitted
--set-mode/--set-ens-namepreserves what the DESTINATION stores, never what the source stores, so promoting a plainipfsstaging build into a publishedipnssite keeps signing its name. - Promoting a site onto itself is refused (a no-op dressed as a promotion, and usually a typo for the other id), as is giving more than one source: the
cid/--from-ipns/--from-sitechoice is now enumerated rather than checked pairwise, so a future fourth source cannot create an unchecked combination. - Rollback falls out for free:
pinnace pin <old-cid> --as <id>with a cid you already know.
readSiteContentCidis exported from the site-management module as the seam this reads through. -
64f4255: Remember superseded builds, and add opt-in retention:
--set-keep <n>and apruneverb.deployandpinreplaced a site's content and pinned the new root, but never accounted for the old one. Every re-deploy left an ORPHAN pin: content the node keeps for ever, that no site references, thatstatuscannot see, and that no pinnace verb could reclaim (the only handle left was a raw Kubopin/rm). Inipfsmode that is one orphan per push.The fix is deliberately two halves, with different defaults:
- REMEMBERING is automatic. A write that changes a site's content records the superseded cid in that site's own
metadata.json(history, most recent first), which is what makes it accountable at all, and makes rollback a normal command:pinnace pin <old-cid> --as <id>. - FORGETTING is opt-in, per site:
--set-keep <n>/--unset-keepondeployandpin, stored in the metadata and PRESERVED when omitted, exactly like--set-modeand--set-ens-name. An absent policy means KEEP EVERYTHING, because pinnace cannot read an ENS record: it knows what a gateway served, never what a contenthash says, so it can never prove an old cid is unreferenced, and a default retention would eventually unpin a live site.
The new
pinnace prune <id> [--keep <n>] [--apply] [--host <name>]applies a policy on its own, across every node. It is a DRY RUN until--apply, and the dry run performs every read and every check, so its report is what a real run would do rather than an estimate. A site with no stored policy and no--keepis refused, not guessed.Two invariants hold on every path:
- Nothing is unpinned that another site currently resolves to. A Kubo recursive pin is not reference-counted, and sites SHARE cids routinely (promoting a staging build with
pin --from-siteleaves two sites on one cid; a rollback re-points a site at a cid still in its own history), so the guard reads every site's current content before unpinning anything, and refuses outright if it cannot list them. A cid skipped this way is reported and stays listed, because it is still held. - A cid leaves the history only once it has actually been unpinned, so a failed unpin is retried by the next prune instead of being forgotten while still occupying disk.
Unpinning only makes blocks eligible for collection; the space comes back on Kubo's own
repo gc, which pinnace still never triggers on your behalf. The verb says so rather than leaving you wondering why the disk did not move.Two behaviour changes worth knowing about, both in the write path: a placement now reads the site's current content cid first (that is the superseded cid), and a fully-stated write (
--set-modeplus--set-ens-name/--unset-ens-name) now also makes ONE tolerant metadata read to carrykeep/historyforward. That read cannot refuse a write: neither field is an addressing decision, and losingkeepfails safe (back to keeping everything), so the existing "a fully-stated write gets past a node whose MFS cannot be read" property is preserved. - REMEMBERING is automatic. A write that changes a site's content records the superseded cid in that site's own
pinnace@0.14.0
Minor Changes
-
39f7c89: Make
install-ciemit a pipeline that actually deploys: nodes as args, a shared composite action, anddeploy --json.The generated GitHub workflow could not run. Its deploy step set
IPFS_API,IPFS_TOKEN,SITE_NAMEandSITE_MODEand then calledpinnace deploy, which reads none of the first three: hosts come from--endpoint/pinnace.json/PINNACE_HOST_<NAME>_ENDPOINTand tokens fromPINNACE_HOST_<NAME>_TOKEN. Every emitted pipeline would have died on its first run with "no hosts", and$SITE_NAMEexpanded empty so the site-id positional vanished too. Its job summary readsteps.deploy.outputs.cid, which nothing wrote. It stayed invisible because the emitter is pure and snapshot-tested as a string, so the snapshot only ever compared to itself: seework/notes/findings/install-ci-emits-a-workflow-the-cli-cannot-execute.md.- INFRASTRUCTURE IS ARGS, NOT ENV. The emitted step bakes your nodes in as literal
--endpoint/--replica-endpointargs and the site as a literal positional, so the only repo secrets are the bearer tokens, named by the CLI's ordinaryPINNACE_HOST_<NAME>_TOKENrule. There is no CI-only env contract: a generated pipeline speaks exactly the surface you speak at your own shell. Emitting with no--endpointdefers to a committedpinnace.jsonand says so. - NEW GLOBAL FLAG
--replica-endpoint <url>(repeatable, publisher-first, only alongside--endpoint), so a whole multi-node setup is expressible with args and no config file. Replicas are numberedreplica-1,replica-2, ... in the order given, which is what names their env-only tokens. This is what makes args-only CI safe: content redundancy comes from the deploy/pin fan-out, and a replica'smirrortimer replicates the signed record and never the content, so a node a run leaves out keeps serving the previous CID. A bare flag, a duplicated replica url, or replicas with no publisher are loud refusals. - NEW COMPOSITE ACTION
wighawag/pinnace/actions/deploy, which both emit targetsuses:. It owns thepinnace deploy --jsoncall, the step outputs (cid,ipns,mode,contenthash,url) and the job summary, so generated YAML cannot drift from the CLI behind it. For anipfs-mode site the summary prints theipfs://<cid>to point ENS at, because each build has its own address. - NEW
pinnace deploy --json: one machine-readable object (cid,mode,ipns, and the per-nodeok/failedbreakdown) instead of the human lines. A partial deploy is data, not just a stderr line, and the exit code is unchanged. install-ciDOES NOT OWN YOUR BUILD.--emit stepsrenders the deploy step alone, to paste into a workflow that already builds however that repo builds. The default whole-workflow target takes--package-manager npm|pnpm|yarn(driving install + cache, withpnpm/action-setupbeforesetup-node) and--build-command, instead of a hardcodednpm ci. The output directory stays stated via--output-dir, never auto-detected.install-cinow installs when asked:--writewrites the workflow and refuses to overwrite an existing one without--force. It still prints by default.
BREAKING for anyone who generated a workflow with the old flags:
install-cinow requires--site <id>, and--build-commandis optional (and refused with--emit steps). Since the old output could not deploy, no working pipeline can regress.The emitter is now tested against its real counterparties rather than a golden string: the emitted YAML is parsed, every action input it passes is checked against the composite action's declared inputs, the token variables are checked against
config-resolution's own naming rule, and an acceptance test feeds the emitted argv to the real CLI dispatch against mock Kubo nodes with only the secrets the report asks for. - INFRASTRUCTURE IS ARGS, NOT ENV. The emitted step bakes your nodes in as literal
Patch Changes
-
4a77748: Document
pinin the README: mirroring an external CID, and the--from-ipnsmigration that had no docs at all.pinnace pinis half the product (it is what makes the boxes a pinning service for content you did not build, not only a deploy target), and--from-ipnsis the one-command migration of an existing IPNS name onto your own nodes. Neither had a walkthrough.--from-ipnswas not even in the command table'spinrow: its only appearance in the whole README was incidental, inside an error-message example illustrating flag strictness, so an operator holding a CID or an old publisher's name could not find out from the docs that the verb they needed exists.- A new "Mirror content you did not build:
pin" section in the package README: mirroring a CID across every node,--host/--no-recursive,--set-mode ipnsfor your own stable name over someone else's content, andsite removeto stop. - A "Migrating from an existing IPNS name" sub-section:
--from-ipnswith the ENS-migration example, the accepted source forms (k51...,/ipns/<id>,ipns://<id>, DNSLink), the exactly-one-source rule, and the two deliberate non-features (you get YOUR name, not the source's key; a snapshot, not a follow). - A "What can go wrong" sub-section: retrievability, the blocking
pin/add, thepin/place/publishstage tags, some-nodes-pinned being a success, the up-frontipns-mode refusals, andpinversussite add. - The command table's
pinrow now carries the--from-ipnssource form, and the root README's pitch names the mirroring capability instead of reading as deploy-only.
Documentation only: no behaviour, flags or output changed.
- A new "Mirror content you did not build:
pinnace@0.13.1
Patch Changes
-
9938995: Fix the IPNS record export:
routing/getwas returning Kubo's JSON envelope, not the record, which broke replica re-announce.Kubo's HTTP RPC does not return raw record bytes for
routing/get. It returns a JSONQueryEventwith the record base64-encoded inExtra; the raw bytes aipfs routing get > fileredirect produces come only from the CLI's text encoder.KuboRpcClient.routingGetwas reading the body as bytes, so it handed back the envelope.That envelope is a 200-OK, non-empty, text body, so nothing upstream failed on it. The publisher wrote it to
/records/<id>.ipns-record, replicas fetched it androuting/putit, androuting/putvalidates IPNS records, so the replica re-announce (the grace window the whole publisher/replica model exists for) could not work. It surfaced only oncestatusstarted parsing records for the sequence number, asrecord is malformed / proto: cannot parse invalid wire-format data.routingGetnow parses the envelope and base64-decodesExtra, and REFUSES loudly (naming the node) when the body is not JSON or carries noExtra, rather than returning an empty record that would later read as "this name has no record".- The signed record is now BYTES end to end.
PublisherFetchreturnsUint8Array, the production fetch usesarrayBuffer(), and the replica cache is read and written as binary. A record is arbitrary protobuf, so any string round trip applies UTF-8 and corrupts it. Only the.ipns-namesidecar stays text. - The test mock gained
routingGetBody(), so the real wire shape is written down once. The old tests seededrouting/getwith raw bytes, encoding a contract Kubo does not honour, which is why this reached production.
Exported records and cached records on existing boxes are rewritten in the correct form on the next
republish/mirrortimer tick.Also makes the dashboard readable: long CIDs and IPNS ids are middle-elided for display (
bafybeigbx…mdakey) with the full value in the link'shrefandtitle, longunknownreasons are truncated with the full text in a tooltip, andword-break: break-allis gone. At eleven columns a 59-character CIDv1 was being shredded into a one-character-per-line ribbon.
pinnace@0.13.0
Minor Changes
-
cd72f21: Surface the IPNS record SEQUENCE, the number that decides which record wins, and document the failover procedure it belongs to.
Among unexpired IPNS records the highest sequence wins, and pinnace reported it nowhere. That mattered most in the one situation the publisher/replica model exists for: when a NEW box starts signing a name that is already live, Kubo looks for the existing record locally and then in the routing system, and if both miss (likely on a box that booted minutes ago) it cannot tell that apart from a genuinely new name and silently starts at sequence 0. That record loses to the dead publisher's for the rest of its ~72h validity while every indicator reads green.
statusnow reportsseqper site, on the CLI line, instatus.jsonand as a dashboard column. It is three-valued: the number when read,unknown (<reason>)when the record could not be read, and absent when the node holds no key for the site. It is never a fallback0, because a spurious0is precisely the failure it exists to expose. Comparingseqacross hosts is how a half-completed failover, or two boxes signing one name, becomes visible.KuboRpcClient.namePublishaccepts an explicitsequence, the corrective lever (Kubo refuses anything not strictly higher than the current record, so it cannot silently regress a name). Deliberately not exposed as a CLI flag.- New
KuboRpcClient.nameInspectdecodes a raw signed record via Kubo'sname/inspect; pinnace decodes no protobuf itself. - New failover runbook at
docs/failover.md, and the README no longer implies that recovering a name means reprovisioning boxes: a replica'sPUBLISHER_ENDPOINTis a URL against the publisher's dashboard vhost, so repointing one DNS record moves every replica to a new publisher with no SSH and nothing to reprovision.
pinnace@0.9.0
Minor Changes
-
f30ee4c: Release as a MINOR: an unknown flag is now a REFUSAL, which changes behaviour for any script passing a stale flag.
- Unknown flag names are rejected. Previously
parseArgsaccepted any--tokenand stored it, so a flag no verb reads was silently ignored. Runningpin ... --mode ipnsafter the--set-moderename parsed cleanly, published no IPNS record, and storedmode: ipfs, which would have maderepublishskip the name until it lapsed. Each verb now declares its flags, an unknown one is a loud error listing the accepted set, and renamed flags get a hint (--modesuggests--set-mode). - eth.limo warming is visible and honest.
statusand the dashboard now PROBE the resolvedhttps://<name>.limo/and report whether it serves, instead of only naming it; andwarmno longer reportswarmedfor a site whose every fetch failed. - An inferred
ensNameno longer displays asnone/unset. A.ethsite with no stored name showed "none" beside a working eth.limo link; it now shows the inferred name marked as inferred, in both the CLI and the dashboard.
Also pins the cloud-init agent version to
0.9.0, the version this release publishes. - Unknown flag names are rejected. Previously
Patch Changes
-
94d6aab: Make eth.limo warming VISIBLE in
statusand HONEST inwarm: an operator can now see whetherhttps://<name>.limo/— the URL a human actually visits — works.statusPROBES the eth.limo URL each site resolves, not just the CID gateway. Every site whoseensName/.ethid resolves a name (the sameresolveEnsNameToWarmrule the on-boxwarmloop uses) getshttps://<name>.limo/probed, and the result appears in all three places the other per-site fields already do: the CLI line (eth.limo alice.eth.limo ethLimoServes=true), thestatus.jsonpayload (ethLimoServes+ethLimoHttp), and the dashboard'seth.limocolumn, which now shows the name AND an ok/no verdict instead of only a name. A site that resolves NO name (anensName: ""opt-out, or a non-.ethid with no explicit name) has nothing to probe and reads as NOT APPLICABLE — absent in the payload,n/aon the CLI line, no verdict on the dashboard — never as a failure. Theannouncedandgatewaycolumns are unchanged, as is theok/unverifiedtoken, which stays about the CID. A probe failure is reported, never thrown:statuskeeps working when eth.limo is down.warmstops claiming success it does not have.pinnace node warmreportedwarmedfor every site regardless of what happened — a site whose every fetch failed still reportedwarmed. It now records what actually occurred, per site:warmed(every attempt succeeded),partly-warmed(some did not — typically the CID gateways are hot but<name>.limois not, the interesting case for a.ethsite),warm-failed(all failed), ornothing-to-warm(nothing was attempted: no gateways configured and no ENS name).ethLimoWarmedon the per-site outcome says which half failed. A cold gateway that ANSWERS (504/404) counts as a failed warm, exactly like one that throws. The load-bearing error policy is unchanged: warming failures are RECORDED, never raised, so a cold or broken gateway still never fails the verb.Library note: the injectable
GatewayProbewas WIDENED from(cid) => Promise<number>to(url) => Promise<number>so one probe seam covers both probed URLs rather than forking a second one; the CID's gateway URL is now built by the status core (cidGatewayUrl), and<name>.limohas a single spelling (ethLimoUrl, beside the rule that resolves the name). Anyone injecting a customgatewayProbereceives a full URL instead of a bare cid. -
c4819d8: Refuse an unknown CLI flag instead of ignoring it: the rule "a flag you type must never mean nothing" now covers a flag's NAME, not just its value.
BEHAVIOUR CHANGE. Every verb declares the flags it accepts, and anything else is a loud error naming the offending flag, listing what that verb does accept, and exiting non-zero WITHOUT performing the operation. Anyone whose scripts pass a stale or misspelled flag will now see a refusal where the flag was previously parsed and silently dropped. That silence was a real, shipped failure: after the
--mode->--set-moderename,pinnace pin --from-ipns <src> --as ronan.eth --mode ipnsparsed, was read by nobody, pinned the site asipfswith no IPNS record published, and storedmode: ipfs, which would have made the on-boxrepublishskip the name until it lapsed. It was caught only because a human noticed a missingipns://line; a CI or cron run would have failed silently. The same now catches plain typos (--set-mod ipns).--modegets an explicit RENAME HINT: ondeployandpinit refuses with "--modewas RENAMED: did you mean--set-mode?". The hint comes from a small static map of this project's renamed flags, and is only offered when the replacement is a flag that verb actually accepts.Nothing valid is refused: the accepted set was enumerated per verb from the code (
provision,deploy,pin,status,derive,install-ci,site,authorize,node,version), the PREFIX-shaped--host-endpoint.<name>/--host-token.<name>stay accepted on every node-touching verb (matched by prefix, for any host name), and the globals--config/--endpointare stripped before the check so they are never reported as unknown from either side of the command.authorizekeeps its own tailored--hostrefusal. Three verb surfaces tighten as a consequence:deploy --host(a deploy fans out to every node by design), and any flag onnode <verb>orversion, are now refusals rather than silent no-ops.The bare-flag refusal is unchanged and composes with this one: the name check runs first, so a bare UNKNOWN flag now reports "unknown flag" rather than "needs a value", while a bare KNOWN flag still says "needs a value".
parseArgsitself is untouched and no arg-parsing dependency was added. -
b9fe50f: Stop telling an operator a site has NO ENS name when it demonstrably has an inferred one.
Both status renderers described the STORED
ensNamefield, but the column is READ as "what is this site's ENS name?". For a.ethsite that stores none, those two answers differ, so the row contradicted itself: the CLI printedronan.eth: ... ensName unset eth.limo ronan.eth.limo, and the dashboard showed a greynonein theens namecell beside a tickedronan.eth.limolink in the next one.The
ens namecolumn now shows FOUR states instead of three, using theensNameToWarmthe report had ALREADY resolved: a STORED name prints as itself; an ABSENT name that resolved anyway prints as the name it warms, MARKED inferred (ronan.eth (inferred)on the CLI line,ronan.ethplus a muted(inferred)hint in the dashboard cell) so it stays distinguishable from a stored name that OVERRIDES the id;""still reads asopted-out/opted out; and a site with nothing stored and nothing resolved still reads asunset/none.Display only. Nothing about resolution, warming or storage moves: the three-valued stored field,
resolveEnsNameToWarmand what the box warms are unchanged, and thestatus.jsonpayload keeps carrying raw, unannotatedensName/ensNameToWarm(the annotation is presentation, so machine readers see exactly what they saw before). The dashboard renderer still resolves nothing itself — it renders the name the report resolved, and a.ethid the report did not resolve still reads asnone.
pinnace@0.12.0
Minor Changes
-
f75279a:
statuscan now say "your ENS name is not pointing at this site" and "eth.limo is serving an older CID".Until now the eth.limo column answered one question: does
<name>.limorespond? A live box answered YES, with every other indicator green, while eth.limo was resolving through a DIFFERENT publisher's name — the operator had pinned the content withpin --from-ipnsand never repointed the ENS contenthash, so pinnace was refreshing an ORPHANED name nothing referenced and the site was one old-publisher outage away from going dark.Each site now reports TWO INDEPENDENT axes, read from the
x-ipfs-path/x-ipfs-rootsheaders of the probe that already ran (no second request, no second seam), in the CLIstatusline, thestatus.jsonpayload and the dashboard:ethLimoOrigin:ours(the path names this site's IPNS id, or its CID for anipfs-mode site),foreign (<path>)NAMING the other name/cid it points at instead,frozen (<path>)(anipns-mode site whose ENS holds an immutable/ipfs/<cid>: correct today, but it will never follow a future deploy),unknown (<reason>), orn/afor a site that resolves no ENS name.ethLimoFreshness:current,stale (<served cid>),unknown (<reason>), orn/a.
They are never collapsed into one verdict — the regression above is precisely a
foreignorigin serving acurrentcid.staleandfrozenrender as neutral ATTENTION states, never the red negative: a gateway lagging a fresh deploy is normal IPNS propagation, not a fault. A missing header or a probe that could not be made reportsunknownWITH its reason on both axes, never a confident negative, and a site with no ENS name reportsn/a(nothing to ask), which stays distinct fromunknown(could not ask).HONESTY, stated in the code docs, the README, the dashboard footer and the glossary: these axes observe what eth.limo RESOLVED AND SERVED through its own cache. They are NOT a read of the ENS record (pinnace speaks no Ethereum RPC — wiring a name into ENS is the consumer's job), so they can lag reality and cannot tell a wrong contenthash from a stale gateway cache.
BREAKING for library consumers who inject their own gateway probe:
GatewayProbenow resolves to{status, headers?}instead of a bare status number, so the ONE probe seam carries the headers these axes need rather than a second probe type being introduced.defaultGatewayProbeand every CID-gateway behaviour are otherwise unchanged.Decisions
Full rationale (alternatives considered, what each touches):
work/notes/observations/ethlimo-origin-and-freshness-decisions.md.- The probe seam was WIDENED once to a result object rather than forked into a header-reading probe beside it, and rather than returning a
number | objectunion that would leave one seam with two shapes. Touches every injector ofGatewayProbe. - This is a
minor, not apatch, answering the open review question about the previous widening of this same exported type: a change to an exported type's shape is a minor, even pre-1.0. frozenapplies to ANY/ipfs/<cid>under a name-publishing site, whether or not that cid is current:originjudges what the record POINTS AT, andfreshnessanswers separately whether it is ours. Folding the cid comparison into the origin axis is the collapsing the two axes exist to avoid.foreignis the only new state painted as a negative. For anipfs-mode site it may be an older cid of your own; from outside there is no way to tell that from another site's cid, and the docs say so.- Cids are compared as strings, so a different encoding of the same cid reads as
stale— one more reasonstaleis an attention state, not an accusation. - The
ok/unverifiedroll-up token is unchanged: a mismatch does not flip it, so a consumer alerting on it must read the two new fields.
pinnace@0.11.0
Minor Changes
-
2425cd7: Give the package ONE version source of truth, and make the cloud-init agent pin derive from it.
pinnace versionnow prints the package's real version, BARE (0.10.0), so$(pinnace version)is directly usable in a script. It used to print the package NAME (pinnace), unfinished scaffolding, so anyone parsing the old output sees a change (both READMEs and the emittedpinnace-setup.shuse it only as a post-install smoke test, which is unaffected).The cloud-init agent pin (
PINNACE_VERSIONin/etc/pinnace-node.env, i.e. what a box runsnpm install -g pinnace@<...>on) is no longer a hand-typed literal: it defaults to the version of the CLI that GENERATED the cloud-init. A box therefore installs the agent matching its generator by construction. The old literal had to be predicted and edited before merging each Version PR, and drifted when that was missed:0.10.0shipped correctly, but0.8.0shipped pinning0.7.0, so a box provisioned from it installed the pre-correction agent. The pin's contract is unchanged in substance: still an EXACT version (never floatinglatest), and a given build always emits the same pin;--pinnace-version/ProvisionInput.pinnaceVersionstill overrides per box.Both consumers read one module,
PINNACE_VERSION(exported from the package root), which resolves the version from the package's ownpackage.jsonat runtime, relative to its own module URL, which is correct in the dev/test path and in the builtdista globalnpm install -g pinnaceruns on a box (proven by building and running the realdistbin in the tests, and by installing a packed tarball into a throwaway global prefix). A missing or version-lesspackage.jsonfails loudly rather than reporting a placeholder version.Decisions
Full rationale (alternatives considered, what each touches):
work/notes/observations/derive-version-from-package-decisions.md; the observation it closes iswork/notes/observations/cloud-init-version-pin-trails-the-release.md.versionprints BARE, notpinnace <version>. The old output was the package name, so there was no contract to preserve; bare avoids forcing every consumer to cut a field. Touches: the two READMEs' smoke test and the emittedpinnace-setup.sh(neither breaks).- DERIVE the pin (option 1 of the observation) rather than enforce the literal in CI (option 2). Removes the failure class instead of policing it, at the cost that the agent version is chosen by the build rather than per release by a human.
DEFAULT_KUBO_VERSIONstays a literal on purpose: Kubo is an external dependency a human chooses; the agent is our own code. Touches:--pinnace-version(now the only way to pin a different agent) and the release ritual (the "bump the pin" step is gone). - Provision tests inject a fixed FAKE pin (
9.9.9); the default is asserted by comparison againstPINNACE_VERSION, never a literal. Otherwise every changesets bump would redden the snapshot on main, trading a once-per-release manual edit for a once-per-release build break. Verified by running the suite green withpackage.jsontemporarily set to99.1.2.