Build the release image and tag it with the commit it was built from.
Usage
mix ymer_node.buildNo options and no arguments. The version comes from mix.exs, the image is
built for the machine's own architecture, and three tags move to it: the
version, latest, and the revision — the short sha of the commit the build
was made from, marked when the tree carried uncommitted changes. That same
revision rides on the image's OCI revision label and inside the node's own
serverInfo.version, so docker images, the label, and a live initialize
all name one commit. Where no revision can be named — no git, or a
directory that is no work tree — the image gets the version tag and latest
only, carries no label, and answers with the bare version.
What moves ymer-node:latest
docker-compose.yml runs that tag and never builds it, so no compose up
moves it. Only a build does — this task, or a plain
docker build -t ymer-node:latest . for a reader without Elixir — or a
deliberate docker tag, which is how an older image is put back under the
name compose reads. A container picks the new image up on the next
docker compose up -d, and mix ymer_node.deploy is what performs that
against the install.
Keeping the image set in hand
A revision tag is minted per commit built, so revision tags accumulate
where the version tag and latest merely move. Nothing here deletes any of
them. Trimming is a judgement about which builds are still worth going back
to — yours to make, not a build's — so the commands live here, beside the
code that mints the tags, rather than running on their own.
What has accumulated, with the dates to judge it by. Read the dates rather than the order: this listing is not sorted by creation time, and it prints it only to the second.
docker images ymer-node --format '{{.Tag}} {{.ID}} {{.CreatedAt}}'Which containers exist and what image each is on — the check to make before removing anything, since a container may be the only thing still holding an image:
docker ps -a --format '{{.Names}} {{.Image}} {{.Status}}'Dropping one revision tag. Without -f, so Docker itself is the safety
net: it removes a tag from an image that has others, and refuses outright
to delete an image's last tag while any container — running or stopped —
still uses it.
docker rmi ymer-node:<revision>Whether superseded images linger at all depends on the image store.
Measured on Docker's containerd store (docker info names it): moving
every tag off an image reclaims it there and then, so nothing is left to
prune. On a storage driver that does leave them, docker image prune
removes what no tag names.
What stops the build, and what only warns
A missing docker, a daemon that does not answer, and a non-zero build all
stop with a message naming the next action. A dirty working tree does not:
the revision records the dirt instead, refusing would block every
build-to-test cycle, and tagging only some of the tags would make the tag
set depend on state the caller never passed. The warning says the image will
carry uncommitted code, and the build proceeds. Where the check itself
cannot run — no git, or a directory that is no work tree — the task says
so in one line and still builds.
The subprocess's own output never stops a build either, whatever bytes it
carries: Mix.Tasks.YmerNode.Build.LiveOutput is where that is kept.
A build that dies inside mix deps.get usually means the network intercepts
TLS, so hex's certificate no longer chains. This image carries no company
certificate by design (the Dockerfile header says why), so the failure
message names that case; the network itself is never probed.
The module is wrapped in if Mix.env() in [:dev, :test], which keeps it
callable under the default :dev and out of the prod release — the release
runs the image this task builds and has no use for the builder.
Summary
Functions
Build the image and apply its tags. Returns the version, the revision —
nil where none could be named — and the tags applied.
The docker arguments a build runs.
Whether docker answers the given arguments with a zero exit.
The tags a build applies: the version tag, latest, and the revision tag
where a revision could be named. The revision tag is the only one that names
a commit rather than a position, so it is the one that accumulates.
A Mix.Tasks.YmerNode.Build.LiveOutput over device. Pass it as
System.cmd/3's :into.
Fail unless docker is on PATH and its daemon answers, and return the
project root.
The revision a build stamps on its image: the short sha of the commit it was
made from, marked when the tree carried uncommitted changes. nil where
there is no sha to name, and the image then carries no revision at all.
The OCI label key a build stamps the revision on.
Whether the working tree at root is clean, dirty, or unreadable — the
state half of tree_status/1, for a caller with no use for the listing.
The tree's state with the git status --porcelain listing that decided
it — "" for :clean and :unknown. mix ymer_node.deploy prints the
listing under its dirty-tree refusal, from the one read that classified,
so what it lists is what the refusal was decided on.
Functions
Build the image and apply its tags. Returns the version, the revision —
nil where none could be named — and the tags applied.
That return is what mix ymer_node.deploy needs: the tags to report, and
the revision to compose the wire version it then holds the running
container to. Callers run preflight!/0 first.
The docker arguments a build runs.
A revision rides twice — as the build argument the runtime stage turns into
an environment variable, and as the OCI revision label — so the label, the
tag and the running node's serverInfo.version cannot disagree. Without
one, neither argument appears: an empty label would claim a revision the
image does not have, and an empty environment variable would leave the wire
version ending in a separator with nothing after it.
Examples
iex> Mix.Tasks.YmerNode.Build.build_argv(["ymer-node:1.2.3"], nil)
["build", "-t", "ymer-node:1.2.3", "."]
iex> argv = Mix.Tasks.YmerNode.Build.build_argv(["ymer-node:1.2.3"], "b2a7152")
iex> Enum.take(argv, 3)
["build", "--build-arg", "YMER_NODE_REVISION=b2a7152"]
Whether docker answers the given arguments with a zero exit.
Public for the same one-home reason as preflight!/0, tree_status/1 and
revision_label/0: mix ymer_node.deploy asks the same question of the
compose plugin and of a revision tag, and two copies would drift into
judging "docker answered" differently.
The tags a build applies: the version tag, latest, and the revision tag
where a revision could be named. The revision tag is the only one that names
a commit rather than a position, so it is the one that accumulates.
Examples
iex> Mix.Tasks.YmerNode.Build.image_tags("3.4.5", "b2a7152")
["ymer-node:3.4.5", "ymer-node:latest", "ymer-node:b2a7152"]
iex> Mix.Tasks.YmerNode.Build.image_tags("1.2.3", nil)
["ymer-node:1.2.3", "ymer-node:latest"]
A Mix.Tasks.YmerNode.Build.LiveOutput over device. Pass it as
System.cmd/3's :into.
Public because mix ymer_node.deploy streams compose's output through the
same collectable; one home for how a subprocess reaches the terminal beats
two that drift.
Examples
iex> Mix.Tasks.YmerNode.Build.live_output(:stdio)
%Mix.Tasks.YmerNode.Build.LiveOutput{device: :stdio}
Fail unless docker is on PATH and its daemon answers, and return the
project root.
Public because mix ymer_node.deploy refuses on the same two conditions
before it does anything else; one home for the checks beats two that drift.
Both entry points call it, and build!/0 deliberately does not — a deploy
would otherwise pay for a second docker info.
The revision a build stamps on its image: the short sha of the commit it was
made from, marked when the tree carried uncommitted changes. nil where
there is no sha to name, and the image then carries no revision at all.
Successive dirty builds of one commit re-point that one tag, the way
latest moves.
Examples
iex> Mix.Tasks.YmerNode.Build.revision("b2a7152", :clean)
"b2a7152"
iex> Mix.Tasks.YmerNode.Build.revision("b2a7152", :dirty)
"b2a7152-dirty"
iex> Mix.Tasks.YmerNode.Build.revision(nil, :unknown)
nil
The OCI label key a build stamps the revision on.
Public because mix ymer_node.deploy reads the same key back off the
install's container to find out which revision it was running. One home for
the key means a rename cannot leave the reader silently finding nothing.
Whether the working tree at root is clean, dirty, or unreadable — the
state half of tree_status/1, for a caller with no use for the listing.
build!/0 warns on it. mix ymer_node.deploy does not borrow it: its
dirty-tree refusal prints the listing, so it reads tree_status/1
instead.
The tree's state with the git status --porcelain listing that decided
it — "" for :clean and :unknown. mix ymer_node.deploy prints the
listing under its dirty-tree refusal, from the one read that classified,
so what it lists is what the refusal was decided on.