@@ -315,7 +315,7 @@ Castle's job is configuration and release management on a running node.
315315 library directories * relatively* — ` filename:join("lib", LibName) ` , so the file
316316 stays relocatable — so every ` lib/<app>-<vsn> ` the handler reads, writes or
317317 deletes resolves there, as does the ` extract_tar(Root, Tar) ` an unpack goes
318- through and the ` erts-<vsn > ` a removal deletes. So a Castle that wrote to
318+ through and the ` erts-<erts_vsn > ` a removal deletes. So a Castle that wrote to
319319 ` $RELEASE_ROOT ` would put the configuration where the handler never looks, and
320320 an upgrade would go on using applications under the installation — a silent
321321 divergence in place of a loud failure. Do not "fix" the guard that way.
@@ -383,6 +383,15 @@ Castle's job is configuration and release management on a running node.
383383 bookkeeping and leaves the applications themselves being extracted into, read
384384 from and deleted out of the emulator's root.
385385
386+ ` EVsn ` there is the * ERTS* version out of the release record, not the release
387+ version, and ` do_remove_release/4 ` deletes that directory only when no
388+ remaining release refers to the same emulator. The two are unrelated numbers
389+ and normally different ones - a release at ` 0.1.1 ` carrying ` erts-16.2 ` - so
390+ prose must not reuse ` <vsn> ` for both. Writing ` erts-<vsn> ` beside
391+ ` lib/<app>-<vsn> ` reads as one version and names a directory that generally
392+ does not exist; it had got into ` remove/1 ` 's ` @doc ` and into the ERTS guard's
393+ refusal message, which ships. Say ` erts-<erts_vsn> ` .
394+
386395 ** The comparison has three answers, not two.** ` compare_dirs/2 ` is
387396 ` Path.expand/1 ` on both and then a ` stat ` on device and inode, and it returns
388397 ` :same ` , ` :different ` or ` {:indeterminate, why} ` . The third is the one to keep:
@@ -515,8 +524,11 @@ Castle's job is configuration and release management on a running node.
515524 more: a diagnostic, not a gate, and nothing has to call it. It stays because
516525 the state it reports is otherwise invisible — the file can be present while the
517526 record the node works from was synthesised — so an operator needs a way to ask
518- that does not unpack or install anything. Whether it belongs in the documented
519- API surface is [ #11 ] ( https://github.com/ausimian/castle/issues/11 ) 's to settle.
527+ that does not unpack or install anything.
528+ [ #11 ] ( https://github.com/ausimian/castle/issues/11 ) settled that it belongs in
529+ the documented surface, and for that same reason: ` bin/castle upgradable ` is
530+ how an operator asks, and a diagnostic nobody is told about is one nobody
531+ thinks to ask.
520532- ** ` unpack/1 ` , ` install/1 ` , ` commit/1 ` , ` remove/1 ` , ` releases/0 ` ** — wrappers
521533 over ` :release_handler ` , with the target version's configuration materialised
522534 before ` install ` and ` commit ` hand it over, the record check inside ` unpack `
@@ -911,20 +923,124 @@ Castle's job is configuration and release management on a running node.
911923 reboot takes and which automation reads.
912924
913925Every one of them is a command entry point, so ` Castle ` is the command
914- boundary: an operation that fails raises ` Castle.Error ` there, which is what
915- leaves a non-zero exit status behind for the shell that asked for it. Raising,
916- not halting — the expression runs on the * running* node, so halting would take
917- down the system under management; ` Kernel.CLI ` catches on the node and
918- re-raises in the calling VM, and only that VM exits. ` Castle.Commands ` holds
919- the operations themselves, returning their outcome instead of acting on the
920- process, which is what makes them testable.
926+ boundary: an operation that fails raises there, which is what leaves a non-zero
927+ exit status behind for the shell that asked for it. Raising, not halting — the
928+ expression runs on the * running* node, so halting would take down the system
929+ under management; ` Kernel.CLI ` catches on the node and re-raises in the calling
930+ VM, and only that VM exits. ` Castle.Commands ` holds the operations themselves,
931+ returning their outcome instead of acting on the process, which is what makes
932+ them testable.
933+
934+ ** ` Castle.Error ` is what ` report!/1 ` raises, and it is not everything a command
935+ raises.** ` report!/1 ` turns a returned ` {:error, message} ` into one; an
936+ exception, a throw or an exit that the operation did not handle goes straight
937+ past it. That is deliberate for the one place it can happen on purpose —
938+ ` installed/5 ` settles the marker and re-raises ` install_release/1 ` 's failure
939+ unchanged, folding it into a message only where the marker could not be settled
940+ — so anything claiming that a failed command raises ` Castle.Error ` , in a ` @doc `
941+ or here, has to say which failures. Automation told to rescue ` Castle.Error `
942+ and nothing else would treat a ` release_handler ` that blew up as a success.
921943
922944` Castle.customize/1 ` is the one function in that module which is * not* one of
923945them — it runs at build time, in a consumer's ` mix.exs ` , and returns a value
924946rather than reporting an outcome. See ** Release integration** below. Anything
925947that says "every function in ` Castle ` " has to say "but ` customize/1 ` ", and the
926948comment at the head of ` lib/castle.ex ` does.
927949
950+ ** What is published and what is hidden follows from that, and is
951+ [ #11 ] ( https://github.com/ausimian/castle/issues/11 ) 's decision.** The whole of
952+ ` Castle ` now carries ` @doc ` and ` @spec ` , and the ` @moduledoc ` says the two
953+ things a reader has to know before calling any of it: that this is the runtime
954+ half of a pair, and that these are commands rather than an API — a command
955+ prints its report and returns ` :ok ` , so the return value carries nothing, and a
956+ refusal raises ` Castle.Error ` rather than returning ` {:error, _} ` . Every command
957+ ` @doc ` names the ` bin/castle ` command that reaches it, because that is the
958+ interface and the function is the thing behind it.
959+
960+ ** Say "a refusal", not "a failure", and the distinction is the one drawn just
961+ above.** ` report!/1 ` turns a returned ` {:error, message} ` into ` Castle.Error ` ,
962+ and that covers every refusal these commands make deliberately — but
963+ ` installed/5 ` re-raises an exception, a throw or an exit out of
964+ ` install_release/1 ` unchanged once the marker is settled, and anything a
965+ dependency raises comes through as itself. So automation that rescues
966+ ` Castle.Error ` alone misses a command that blew up rather than refused. This
967+ paragraph exists because the blanket version of the claim was written here in
968+ the same commit that corrected it in the ` @moduledoc ` , three paragraphs apart.
969+
970+ Two decisions inside that. ` make_releases/0 ` is ` @doc false ` : its only caller is
971+ the launcher's ` env.sh ` fragment, in the preboot VM of a ` start ` or ` daemon `
972+ whose deployment has no ` RELEASES ` yet, and by hand it either does nothing (the
973+ file is there) or does what the next start would do anyway. Its contract is with
974+ a shell fragment in another project, so publishing it would document a function
975+ nobody should call. It keeps its ` @spec ` regardless — the spec is the contract
976+ whether or not the function is published. And ` install/2..5 ` is documented as
977+ what it is, a seam the concurrency test drives: one ` @doc ` covers every arity of
978+ a clause with defaults, so saying nothing about the extra four would leave them
979+ reading as an API. ` running/1 ` is a third case, and documented rather than
980+ hidden for a different reason than the other two. It has no subcommand either —
981+ ` bin/castle ` dispatches exactly ` releases ` , ` upgradable ` , ` unpack ` , ` install ` ,
982+ ` remove ` and ` commit ` , and ` running/1 ` 's only shipped caller is the confirmation
983+ loop inside ` bin/castle install ` . But unlike ` make_releases/0 ` it answers a
984+ read-only question and mutates nothing, and automation driving ` rpc ` rather than
985+ ` bin/castle ` needs precisely it to know when an install has finished booting.
986+ Hiding it would strand that caller; publishing it costs nothing. Every remaining
987+ function is a command an operator invokes, and hiding one of those would
988+ document nothing useful anywhere.
989+
990+ The specs say ` :: :ok ` and nothing more, because that is what ` report!/1 `
991+ returns; a spec naming the lines, or an error tuple, would be describing
992+ ` Castle.Commands ` . Nothing checks them — there is no Dialyzer here — so they are
993+ kept by hand, and a claim in a ` @doc ` about what a command refuses is worth
994+ checking against ` Castle.Commands ` before it is trusted.
995+
996+ ** A definition with defaults needs one ` @spec ` per arity, and "every public
997+ function carries a spec" is a claim about arities.** ` install/1..5 ` is five
998+ functions and therefore five specs; the first version of this carried two, for
999+ ` install/1 ` and ` install/5 ` , and left three unspecced while this file and
1000+ ` RELEASE.md ` both said the surface was complete. Nothing catches that, and
1001+ ` mix docs ` is no help either: ExDoc renders one spec against a defaulted
1002+ definition, the widest arity's, so the page looks the same with two specs as
1003+ with five. ` credo --strict ` passes, and there is no Dialyzer. So check it with
1004+ ` Code.Typespec.fetch_specs(Castle) ` and count — against
1005+ ` Castle.__info__(:functions) ` , which is the list that has to be covered —
1006+ rather than by reading the source, which is what missed three of them.
1007+
1008+ ** The ` @doc ` s are claims, nothing checks them, and the first draft of them
1009+ walked back into two rules this file had already settled.** Both are worth
1010+ naming, because both were regressions rather than staleness:
1011+
1012+ * ` upgradable/0 ` 's said the record is synthesised when the system started
1013+ "without a ` releases/RELEASES ` file it could read" — which is the
1014+ * readable* phrasing corrected above, admitting the malformed-terms
1015+ counterexample, and it named the file unqualified besides. A ` @doc ` that
1016+ describes a refusal has to say what the refusal says: name the authority
1017+ (the file ` :release_handler ` accepts, no single property of it being the
1018+ test), and qualify the path with ` RELDIR ` and ` {sasl, releases_dir} ` .
1019+ * ` commit/1 ` 's said that committing an already-permanent version "succeeds
1020+ and changes nothing", which was true of the shape castle #14 replaced.
1021+ ` Commands.commit/5 ` materialises inside its own serialised region, so the
1022+ target's ` sys.config ` is rewritten from current provider inputs before
1023+ ` make_permanent/1 ` — which is itself a no-op for that version — is called
1024+ at all. OTP's step being idempotent is not the command being idempotent.
1025+
1026+ Three more were ordinary staleness of the same kind, and the pattern is that a
1027+ ` @doc ` describing a * sequence* goes stale where a ` @doc ` describing a value
1028+ does not: ` install/1 ` 's claimed two steps before ` :release_handler ` was asked
1029+ for anything, when ` install_upgradable/5 ` asks ` which_releases/0 ` first and
1030+ refuses for a pending marker in between, and the line that matters is
1031+ ` install_release/1 ` rather than any handler call; the rollback an install
1032+ leaves was described as "anything that takes the system down brings that one
1033+ back", which is false for the one reboot a restart install has already asked
1034+ for and the launcher is holding a marker to carry out; and ` unpacked ` was
1035+ glossed as "staged and never installed", which ` running/1 ` 's own ` @doc `
1036+ already contradicts. ` bin/castle commit ` 's argumentless form was described as
1037+ defaulting to "the version running now", where ` castle.sh.eex ` selects a
1038+ ` :current ` release and exits non-zero when there is none.
1039+
1040+ So: read a ` @doc ` about a sequence against the function that implements the
1041+ sequence, not against the module's summary of it, and read one about
1042+ ` bin/castle ` against Forecastle's ` priv/castle.sh.eex ` .
1043+
9281044Forecastle is what arranges for these to be reachable: it leaves the
9291045configuration Mix wrote alone, adds a ` :preboot ` script that starts ` :castle ` ,
9301046and writes the ` env.sh ` fragment and ` bin/castle ` wrapper that call into this
@@ -1418,13 +1534,16 @@ wrote, so Elixir's pipeline is still armed in the file the launcher reads.
14181534 it. It is the one limitation here that a lock cannot narrow, which is why the
14191535 filesystem half of the protocol — ` publish/2 ` refusing rather than replacing —
14201536 has to stand on its own.
1421- - ** The public API is undocumented, apart from ` customize/1 ` .** ` @moduledoc ` is
1422- still the generated placeholder and the commands carry no ` @doc ` or ` @spec `
1423- annotations ([ #11 ] ( https://github.com/ausimian/castle/issues/11 ) ).
1424- ` Castle.customize/1 ` has both, and was documented with #12 rather than left
1425- for #11 because it is the function a consumer's ` mix.exs ` calls and nothing
1426- else says how. It is the standard the rest has to be brought up to, not an
1427- exception to be levelled down.
1537+ - ** Nothing checks the ` @spec ` s.** The public surface is documented as of
1538+ [ #11 ] ( https://github.com/ausimian/castle/issues/11 ) — see the end of * What it
1539+ does* for what is published and why — but there is no Dialyzer in this
1540+ project, so a spec that stops describing its function fails nothing. They are
1541+ all ` :: :ok ` today, which is the whole of what ` report!/1 ` returns, so the way
1542+ one goes wrong is a command that starts returning something else and a spec
1543+ that keeps saying ` :ok ` . The same holds for what the ` @doc ` s claim a command
1544+ refuses: ` mix docs ` catches a broken * reference* , and nothing at all catches a
1545+ true sentence that has stopped being true. Both are read against
1546+ ` Castle.Commands ` by hand.
14281547- ** The README is out of date.** It documents an ` :appup ` compiler and a
14291548 ` mix castle.relup ` task that moved to Forecastle in 0.3.0, the release
14301549 management commands it describes on ` bin/<release> ` now live on ` bin/castle ` ,
0 commit comments