Guile Automate helps you build and run pipelines written in GNU Guile using GNU Guix and Spritely Goblins. A pipeline is written as Guile definitions, lowered to a Guix store item, and run as Goblins actors with each step in its own process. A pumphouse holds pipelines, their run history and shared resources, and serves a web interface over them.
Automate is a prototype. It runs its own pipelines, but any version may change its interfaces, the pumphouse’s databases or the form of a pipeline definition without keeping the old ones working. The Version History appendix (see Version History) says what changed in each version and what to do about it. Report bugs in the issue tracker.
This chapter explains how the pieces of Guile Automate fit together. Each section covers one concept with a short code example. For the full list of procedures, fields and options, see API.
A pipeline is set out in Guile code. The steps are the parts of the pipeline, and these reference the outputs of other steps, along with resources and parameters. The pipeline itself is a collection of steps. Lastly you turn a pipeline into something that can be run. The file below writes a greeting into a directory in one step and prints it in another.
(use-modules (guix gexp) (automate)) (define work (workspace-resource #:name 'work)) (define who (param 'name)) (define written (step (name 'write) (inputs (list work who)) (body #~(let ((directory (attach #$work))) (call-with-output-file (string-append directory "/greeting") (lambda (port) (format port "Hello, ~a!" #$who))) directory)))) (define shown (step (name 'show) (inputs (list written work)) (body #~(let ((directory (attach #$work))) (call-with-input-file (string-append directory "/greeting") get-string-all))))) (one-shot (pipeline 'examples/hello #:steps (collect-steps shown)))
Each step’s body is a G-expression, the quoted code Guix builds with
(see G-Expressions in GNU Guix Reference Manual). It is
written at definition time and runs later, in a process of its own.
The #~ marks the body; #$ inside it splices in a value
from the definition, such as a package or one of the step’s inputs.
attach and the rest of the body vocabulary are described in the next two
sections.
The file is not run directly (see System Requirements for what the machine needs). It is built with Guix, which lowers the final expression to a program in the store:
$ guix build -f hello.scm /gnu/store/...-automate-one-shot-examples-hello
Automate’s modules have to be on Guile’s load path for this, either
through an installed package or with -L naming a checkout.
Building does not run any step: it checks the definition, writes
each step’s body into a program with its own dependencies, and
writes the structure of the pipeline beside them. The result is a
program that runs the pipeline once:
$ $(guix build -f hello.scm) --param-string=name=World
A parameter is given with --param-string=NAME=STRING, whose
value is the string after the ‘=’, or with
--param=NAME=DATUM, whose value is read as Scheme
(see Parameters).
--secrets=DIR names the directory secret files are read from
(see Resources). --repl=STEP stops that step and offers a
session on it (see Attaching to a Step). --cap=N bounds
how many step processes run at once; the default is derived from the
CPU count.
What a step prints is its log. A log is not an output: an
output is a value a step provides for other steps (see Outputs and Errors). The program copies each step’s log to its own standard
output as the step writes it, with secrets masked. Steps that run at
the same time have their lines mixed together, with no step name on
them. The program also prints each event of the run as it happens,
as a Scheme datum. --quiet leaves out the events but not the
logs. At the end it prints the run’s outcome and the status of
every step, and it exits with a non-zero status when the run errored.
Nothing is kept once the program exits, unless it is told where a
pumphouse is (see Running a Pumphouse). The logs then go to the
pumphouse rather than to the terminal, and automate run
output prints them.
The same pipeline runs from a pumphouse when the file ends in
run-pipeline-daemon instead of one-shot, which
Pipelines explains. A definition file is an ordinary Guile
program up to its last expression, so steps can be generated with
map, shared helpers can live in a module of the project’s own,
and a procedure can return a whole pipeline.
The examples/ directory of the source holds more definition
files, each with the commands that build and run it beside its last
expression. hello.scm adds a secret and a limiter to the file
above, and pipeline-daemon.scm serves it from a pumphouse.
linked.scm has one pipeline call another, and channels.scm
builds a package from a guix-channels resource. slow.scm
runs long enough to watch, and pumphouse.scm runs a pumphouse for
the others. pipeline-shepherd.scm runs a Shepherd for the
pipeline daemons, and shepherd-bridge.scm a bridge of the user’s
own Shepherd.
A step is declared with the step form, giving its fields by
name in any order:
(step (name 'build) (inputs (list cloned work)) (label "Build the release") (body #~(...)))
The name is a symbol, unique within the pipeline. The body is
required; every other field has a default. (inherit STEP)
copies another step’s fields before the ones given, so a variant is
declared by its differences. The field names are bound as variables
inside the form, so a definition called name, inputs
or body is shadowed there: call the variable something else.
The full list of fields is in (automate).
The inputs of a step are a list of sources: other steps, resources and parameters. A step runs once its inputs have settled, so listing a step as an input is how one step is made to run after another. Steps whose inputs allow it run at the same time, each in a process of its own.
Inside the body, an input is referred to by splicing its source with
#$, as a package is spliced. For a step, #$built is its
value output, which is what its body returned. For a
parameter, it is the value the run was given. For a resource, it is
a reference that the body attaches to get something it can
use, such as a directory or a file (see Resources).
(define work (workspace-resource)) (define built (step (name 'build) (inputs (list work)) (body #~(attach #$work))))
A step’s other outputs are selected as Guix selects an output of a
package, with a colon: #$built:version is the version
output of built, and so is
#$(step-output built 'version). The step declares that it
wants the output with step-output in its input list:
(define reported (step (name 'report) (inputs (list (step-output built 'version))) (body #~(format #t "built version ~a~%" #$built:version))))
Listing a step plainly selects its value output. A step that
declares outputs has only the ones it declares, so a consumer of
built above lists (step-output built 'dist) rather
than built; the pipeline is refused otherwise, naming the step
and the output. A step may take several inputs from one source,
selecting different outputs. No step may declare an output called
out, since #$built:out could not be told from a plain
#$built.
(input-present? #$built) says whether the input is there,
without raising when it is not.
A splice says which input it means with the binding the file already
has in hand, so nothing has to agree on a name. This is what lets
steps written apart be put together. Two steps from two places may
each hold a workspace called work: a pipeline gives the second
of them the identifier work.2, and each body reaches its own.
A name that no other source wanted is kept as it is, so a pipeline
with no such pair is unaffected.
A step and a parameter are still named from outside the run, on the command line and in the web interface, so two steps cannot share a name and a step, a resource and a parameter cannot share one either.
The inputs a body splices are checked when the file is built. A body that splices a source its step does not declare is refused then, with the step and the name, rather than failing at run time.
A body is also compiled when the file is built, in a module of its
own called (automate step-body name). A body Guile
cannot compile is refused then, and Guile’s warnings about one, such
as a variable it cannot find, are printed as the file is built. The
backtrace on a step’s error names that module, giving the line and
column of each frame, so a failure can be placed without running the
step again.
A body runs in a process of its own, under the Guile that Guix is
built with, and it closes over nothing from the definition file. It
sees three kinds of things: values spliced in with #$ when the
file was built, its inputs, and the body vocabulary of
(automate pipeline step):
input-present?, provide, attach,
release, commit, store-item, mask,
trigger-run, list-runs, listed-run-output,
abort-run, run-id, run-url and in-vat.
run-id is the id of the run the step is part of, and
run-url the address of the run’s page on the pumphouse’s web
interface, or #f when no pumphouse with one serves the run, so
a step reporting to a forge can link back to the run.
Software is given to a body the way Guix gives it to a build: a
package spliced in with #$, usually through file-append
to name a program in it. Guix’s build utilities, which give
invoke, mkdir-p and copy-recursively, are
imported into every body, along with (ice-9 match),
(ice-9 textual-ports) and (srfi srfi-1).
(use-modules (guix gexp) (gnu packages version-control) (automate)) (define work (workspace-resource #:name 'work)) (define repository (param 'repository)) (define cloned (step (name 'clone) (inputs (list work repository)) (body #~(let ((directory (attach #$work))) (invoke #$(file-append git-minimal "/bin/git") "clone" "--depth=1" #$repository (string-append directory "/source")) (string-append directory "/source")))))
Other modules are named in the modules field, which adds to
the default set:
(step (name 'hash) (inputs (list cloned)) (modules '((rnrs bytevectors) (ice-9 binary-ports))) (body #~(...)))
Guix’s own modules are available to every body without being listed
as software, since Guix is part of every step’s program, so a body
can talk to the Guix daemon with (guix store) once it names the
module and holds the guix-daemon resource (see Resources).
That Guix is a library, not a package collection: a body takes
packages from the guix-channels resource, which says whose
collection they are.
Helpers that several bodies share go in a module of the project’s
own, beside the definition file, and each step using it names it in
modules. Building the file copies into a step’s program every
named module that no package provides: Automate’s own, and any found
on the load path outside the store, so guix build -L . from
the project’s checkout finds both. Such a module is build-side code
in Guix’s sense: it runs in the step process, may call the body
vocabulary and Guix’s modules, and holds no package objects.
;; release/helpers.scm (define-module (release helpers) #:use-module (ice-9 textual-ports) #:export (read-first-line)) (define (read-first-line file) (call-with-input-file file (lambda (port) (let ((line (get-line port))) (if (eof-object? line) "" line)))))
(define versioned (step (name 'version) (inputs (list cloned)) (modules '((release helpers))) (body #~(read-first-line (string-append #$cloned "/VERSION")))))
Automate’s own helpers are the (automate helpers …)
modules of the API chapter: (automate helpers git) runs
git, (automate helpers secrets) reads an attached secret,
(automate helpers profiles) builds a development profile
through the Guix daemon and runs shell text inside one, and
(automate helpers notify) sums up a run’s checks and sends
messages about them (see Notifications). The programs they run
come from Guix the first time a step needs them. They are built
through the Guix daemon from the Guix of a guix-channels
resource the body attached (see Resources). A body can instead
set current-git or current-bash to a program it chose
itself, which is how a pipeline pins or replaces one. Here the
package is git-minimal, spliced in from the definition file,
and git is the helper that runs it:
(parameterize ((current-git #$(file-append git-minimal "/bin/git"))) (git (attach #$work) "fetch" "origin" #$ref))
A token for the forge stays in its secret file. A body sets
current-git-credentials to the path attach of the
secret returns, around the calls that reach the remote, and git reads
the file through a credential helper when the remote asks. The URL
is the repository’s plain one, so it can be the same parameter for a
run against the forge and a run against a clone on disk, and no token
reaches a command line, an error record, a log line or a
.git/config:
(parameterize ((current-git-credentials (attach #$forgejo-token))) (git (attach #$work) "clone" "--depth=1" #$repository "trunk"))
A body is sequential Guile on a thread of its own. It may block on
subprocesses and files for as long as it likes; the connection back to
the pipeline stays live on another thread. A step with a
timeout, in seconds, is killed when it runs out and recorded
as errored.
A step process is given an environment of Automate’s making, not the
one the pipeline process runs with. It holds LANG, the
certificate variables the machine sets, TZ and TMPDIR,
and, for a step declaring a guix-daemon resource, the
GUIX_DAEMON_SOCKET that resource names, with
GUIX_CONFIGURATION_DIRECTORY where the pipeline process has it
set. Nothing else: a step has no HOME and no PATH.
That is why a body names a program in full, with #$ and
file-append, rather than running it by name. Without
HOME, a step also reads none of the operator’s dotfiles, so
no ~/.gitconfig, credential helper or SSH key reaches it
because it happened to be on the machine. Where a step runs a shell
script in a profile, invoke-in-profile of
(automate helpers profiles) sources
that profile and sets PATH from it, so the script finds the
profile’s programs and nothing else.
Without HOME there is also nowhere for a program to keep a
cache, and a program that wants one usually carries on without saying
much. Guile compiles a module as it loads it and writes the result
under XDG_CACHE_HOME, or ~/.cache when that is unset; in
a step it can create neither, warns once per file, and interprets the
module instead. That is slower, and a backtrace taken from
interpreted code names no source of yours, so a step that runs Guile
code — a test suite, most of all — wants a cache directory of its own.
TMPDIR is the step’s own writable temporary directory, so this
is enough, ahead of whatever the step runs:
export XDG_CACHE_HOME="$TMPDIR/cache"
The same goes for anything else that looks for a home: point it at a
directory under TMPDIR, which lasts as long as the step and no
longer.
An operator who has to get a further variable to the steps of a run
names it with --pass-environment=NAME, which passes it on if
the pipeline process has it. Reach for it rarely: a variable passed
that way reaches every step, and it is authority that the pipeline
does not declare and a reader of it cannot see. What a step needs
from outside the run is better a secret, a parameter or a resource.
A step runs in a file system of its own. It holds the store, a temporary directory of its own at /tmp, a shell at /bin/sh, the /dev and /proc any program expects, and, of the rest of the machine, only what the step’s inputs name: its workspaces, its secrets, the Guix daemon’s socket. Everything else is absent, and the root is read-only, so a step writes only where it was given somewhere to write.
The few files the step’s environment names come with it: the zone
TZ is read from, and the certificates SSL_CERT_DIR,
SSL_CERT_FILE, GIT_SSL_CAINFO and CURL_CA_BUNDLE
point at. A step is never told where a file is and then left unable
to open it.
The paths are the machine’s, so attach answers what it always
did and a step that declares what it uses needs no change. A step
that reads a path nothing gave it fails there, with the error its
program gives for a file that is not there. That includes the other
secrets in the directory the run was given: a step holding
(secret-resource 'token) sees that file and no other.
A step holding the guix-daemon resource is the exception worth
knowing. The socket is the whole store with write access, so what a
step can reach through it is not bounded by its file system at all.
An operator who has to give the steps of a run a directory the
pipeline does not declare names it with --expose=DIR. As with
--pass-environment, reach for it rarely: it goes to every step,
writable, and it is authority that a reader of the pipeline cannot
see. Where the directory is the pipeline’s to name rather than the
operator’s, a file-input resource says the same thing in the
definition, read-only and only to the steps that declare it.
A parameter naming a path on the machine is where this catches a
pipeline out. A repository parameter is usually a URL, which a step
with the network reaches, but a run given a local clone instead is
given a path nothing declared, and the step fails on it as it would on
any other path it was not given. --expose is the answer, since
which path it is was the operator’s to choose and not the pipeline’s.
The label is a string the web interface draws on the step’s
node in place of its name, and the group is a symbol that
gathers steps into a cluster in the drawing. Neither affects how the
step runs.
A step that declares no outputs has one, called value, provided
from what its body returns. A step that does more than one thing
declares its outputs and provides each by name:
(define built (step (name 'build) (inputs (list cloned)) (outputs '(dist version)) (body #~(let ((source #$cloned)) (invoke "make" "-C" source) (provide 'dist (string-append source "/dist")) (provide 'version "1.0")))))
provide returns once the pipeline has recorded the output, so
a step downstream can start on it while this body carries on.
Providing the same output twice is an error. When the body ends,
a declared value not yet provided takes the body’s return
value; any other declared output left unprovided errors the step,
after its work is done. So a step that declares dist and
never reaches provide for it has errored even if the body
returned normally.
An output’s value is plain data: a string, number, symbol, boolean
or list of them. A store item from the guix-daemon resource
is the one kind of reference a step may provide (see Resources).
Bulk results go into a workspace or the store, and the step provides
the path.
An input settles when the output it selects is provided, or when its step has finished without providing it. A step waits until every input has settled. It then runs if every required input was provided, and is skipped otherwise. A skipped step provides nothing, so the steps that need its outputs are skipped in turn.
#:required? #f on an input makes the step wait for it without
depending on it. In the body, input-present? says whether
the value came:
(input-present? #$built) (input-present? #$checked:status)
This is how a step runs after a fan-out whatever the outcome of each branch:
(define architectures '(x86_64 aarch64)) (define builds (map (lambda (architecture) (step (name (symbol-append 'build- architecture)) (inputs (list cloned)) (body #~(string-append #$cloned "/dist/" (symbol->string '#$architecture))))) architectures)) (define reported (step (name 'report) (inputs (map (lambda (build) (step-output build 'value #:required? #f)) builds)) (body #~(begin #$@(map (lambda (architecture build) #~(format #t "~a: ~a~%" '#$architecture (if (input-present? #$build) #$build "not built"))) architectures builds)))))
The list of builds is known when the file is built, so the body is
made there: #$@ splices in one expression per build, each
splicing its own step. Every input the body refers to is then checked
when the file is built.
When a body raises, the step is errored and its implicit
error output is provided with a record of the error: its kind,
its message, its irritants printed as strings, and the printed
representation of the original. Outputs provided before the raise
stand. Any other step may take the error output as an input,
and runs only when the error happens:
(define published (step (name 'publish) (inputs (list (step-output built 'dist))) (body #~(upload #$built:dist)))) (define rolled-back (step (name 'rollback) (inputs (list (step-output published 'error))) (modules '((automate pipeline errors))) (body #~(begin (format #t "publish failed: ~a~%" (error-record-message #$published:error)) (undo-upload)))))
The record is a list, and (automate pipeline errors) has the
accessors for it. A step that wants to run whether or not another
failed takes both value and error with
#:required? #f, and asks input-present? which came.
A step is pending until its inputs settle, running
while its process lives, and then one of succeeded,
errored, skipped or abandoned, the last meaning
its process died before it finished. These are the words
automate run show and the run’s page use.
A run is errored when a step was abandoned, or when a step errored
without (error-permitted? #t), or when the run was aborted. It
is abandoned when the process running it, a pipeline daemon or
a one-shot program, died or was restarted before it finished
(see Running a Pumphouse); its running steps are then abandoned and
its pending ones skipped. Otherwise it
succeeded.
Skipped steps never fail a run. This is deliberate, and it has a
consequence to watch for: a pipeline whose final step is skipped
because an earlier one errored is only failed by that earlier error.
If the earlier step is marked error-permitted?, the run
succeeds with its last step never having run. So error-permitted?
is a claim about the step that raises: its error is expected, and
something downstream handles it. Put it on the step whose failure is
routine, not on the handler. A handler taking the error
output keeps the default, so that a failure in the handling itself
fails the run.
A step’s timeout counts as an error of the step, not as abandonment.
automate run abort RUN-ID ends a run early: its pending steps
are skipped and the run is errored.
A resource is something a step needs that the pipeline provides: a
directory to work in, a secret, a file to read, the network, a slot,
the Guix daemon, the Guix its packages come from, a decision from
outside the run. Resources are declared once,
with a name, and appear in the input lists of the steps that use
them. A body gets at one with attach:
(define work (workspace-resource #:name 'work)) (define token (secret-resource 'forge-token)) (define fetched (step (name 'fetch) (inputs (list work token)) (body #~(let ((directory (attach #$work)) (token-file (attach #$token))) (fetch-into directory (call-with-input-file token-file get-string-all))))))
attach returns whatever identifies the resource locally: a
path for a workspace, a cache, a secret, a file input or the Guix
daemon’s socket, and nothing of use for a limiter or a gate, where
attaching is the point.
Attaching twice from the same step returns the same thing. Everything
a step attached is released when its process exits; release
lets it go earlier, which matters for limiters. A failure to attach,
such as a secret whose file is missing, raises in the body and errors
the step.
The one binding is what identifies a resource. Two steps sharing the
binding work share the directory. Two separate
(workspace-resource) forms are two resources: a pipeline holding
both gives the second the identifier workspace.2 and keeps them
apart, and a body reaches the one it means by splicing it (see Steps and Inputs). The identifier is what the run shows and what a
workspace’s directory is named after.
A resource’s identifier starts from its name, which is its type unless
#:name gives another, and is numbered where a step, a parameter
or another resource of the pipeline has that name already. So the
name is a label, for the run’s pages and events. It is more only
where something outside the definition picks the resource by it: a
secret, found by name among the files the run is given, a gate, which
automate gate approve names, and a pumphouse limiter, which
the pipelines sharing it agree on by name.
Those take the name as an argument of their own.
A resource is one object in the pipeline process serving the pipelines that list it, a pipeline daemon or a one-shot program, set up when that process starts and reached by every run there. What it keeps for a run and what it shares between runs is the resource’s own affair: a workspace is a directory per run, gone at the run’s end, while a limiter’s slots and a cache’s directories are held across runs. A resource defined once and listed by two pipelines is one object in a pipeline daemon serving both. The constructors are in (automate).
(workspace-resource) is a directory, made when first attached and
shared by every step holding it. It is the usual place for a checkout
and everything built from it. Steps holding a workspace see the same
directory, so one step’s files are the next step’s inputs by path.
A workspace is scratch, and belongs to the run that made it. Every
run makes a directory of its own to hold its workspaces, under
TMPDIR or under /var/tmp where the environment names
none, and removes it when the run ends, whatever the run’s outcome.
Nothing a run leaves in a workspace outlives it: what has to stay on
the machine is a file output, below, and what has to come from the
machine is a file input. Point TMPDIR at a file
system with room for what the steps build; a /tmp that is a
tmpfs is memory.
#:seen-at says where a step sees it, rather than leaving it
where the run put it:
(define work (workspace-resource #:seen-at "/src"))
attach then answers /src, in every step holding the
workspace, and where the directory is on the machine is the run’s
business rather than the pipeline’s. Put it on the resource and not
on a step, so that every step agrees: a path one step provides as an
output is a path the next one opens. A pipeline naming one place for
two resources a step holds is refused, since the step could only see
one of them. #:seen-at has no effect on a run whose steps are
not confined, which has no file system of its own to put it in.
(secret-resource NAME) is a read-only file, found under the name
in the directory the program was given with --secrets=DIR.
Attaching returns its path; the step reads it. The file has to exist
when the step attaches it, and the step errors otherwise. Where the
secret material comes from is outside Automate: an operator puts the
files in place. A secret is never a parameter, since parameters are
recorded and shown.
#:seen-at says where a step sees it, as it does for a
workspace:
(define token (secret-resource 'forge-token #:seen-at "/etc/forge-token"))
A step is given the secrets it declares whether it attaches them or
not, so the run masks a declared secret’s contents from the moment the
step starts. Attaching it masks them too, for a secret whose file
appears later. Wherever the value would appear in what the run shows,
the marker
*** stands in its place: in the step’s log as it is written,
in every event, in every output value a step provides, and in every
error record, including the irritants of an error a body raised. The
whole value is masked, whitespace trimmed, and so is each of its lines
when it has several, as a private key does. The masking is exact: a
value printed in another encoding, or in pieces, is not recognised.
And a value that was printed at all has already left the step
process, so the masking is a net under the rule that a body does not
print a secret, not a reason to relax it.
A value under eight bytes is not masked, since replacing it would
corrupt ordinary output, and the run records a mask-refused
event naming the secret, as it does for a value over four kilobytes
without lines short enough, or past the run’s limit of 128 values.
A body that derives a further secret, such as a short-lived token it
exchanged the secret for, masks it with mask before printing or
providing it:
(define exchanged (step (name 'exchange) (inputs (list token)) (modules '((automate helpers secrets))) (body #~(let ((session (exchange-token (read-secret (attach #$token))))) (mask session) session))))
mask raises, with the reason, for a value the run cannot
take, so a body finds out at once rather than leaking quietly.
(file-input-resource #:source PATH) is a file or directory on the
machine the run is on, which the steps holding it read and cannot
write. Attaching returns its path, and the step opens it as it would
any other:
(define sources (file-input-resource #:name 'sources #:source "/srv/sources")) (define built (step (name 'build) (inputs (list sources work)) (body #~(build-from (attach #$sources) (attach #$work)))))
The source has to be there when a step attaches it, and the step errors otherwise: the resource makes nothing, it only lets a step in. A step that holds the file input sees it and no more of the machine than it saw before, and writing there is refused by the kernel rather than by convention, so a pipeline can hand a step a checkout, a cache or a directory of fixtures without also handing it the means to change them.
#:seen-at says where a step sees it, as it does for a
workspace or a secret:
(define sources (file-input-resource #:source "/srv/sources" #:seen-at "/src"))
Reach for it rarely. A pipeline naming a place on the machine runs
only on a machine that has it, which is what a workspace, a step’s
output or a store item avoid; and where the place is the operator’s to
choose rather than the pipeline’s, --expose is still the
answer. What a file input is for is the case where the place is the
pipeline’s to name, the same on every machine it runs on, and the
steps have no business writing to it.
Neither the read-only view nor #:seen-at has any effect on a
run whose steps are not confined: such a step has no file system of
its own, and reaches the source whether it declares it or not.
(file-output-resource #:destination PATH) is a place on the
machine that the run replaces, and the one thing a step writes whose
result outlives the run. The step never holds the destination.
Attaching answers a path in a staging directory of the run’s own, the
body makes the content there, and commit publishes it:
(define site (file-output-resource #:name 'site #:destination "/srv/www/site")) (define published (step (name 'publish) (inputs (list site checked)) (body #~(let ((out (attach #$site))) (build-the-site-into out) (commit #$site)))))
The publishing is the pipeline process’s, not the step’s. A confined
step never has the destination’s directory in its file system at all,
so it can write what it stages and nothing else; the authority to
replace the place on the machine stays with the process the operator
started. That is the difference from --expose, which hands a
step the directory itself and says nothing a reader of the pipeline
can see.
Commit publishes in one step that a reader of the destination cannot see the middle of. The destination is a symbolic link that Automate keeps: each commit puts a generation beside it and renames a new link over it, so a reader following the destination gets the generation a run published or the one before it and never a half-written tree. Renaming the content itself onto the destination would not do, since that is refused for a directory that is not empty, and replacing a built directory is what this is for. A destination that is already a directory is in the way: an operator moves it aside once, and the run makes the link.
commit raises where nothing was staged, where the file output
was committed already—it is committed once—and where the
destination cannot be published to. The raise happens in the step, so
a publish that fails errors that step, skips what depends on it and
lands in that step’s log with a backtrace, rather than surfacing
somewhere the run has no name for. Whether the run publishes only
after everything else passed is the step graph’s to say, by what the
publishing step depends on, as it is for a step that pushes to a
forge.
#:commit-by says what commit publishes:
renameThe default. The generation is the staged content, moved. It costs nothing, and no path the step can still name reaches what was published—but a descriptor the body left open on a staged file still writes to the published one.
copyThe generation is a copy, so the body shares nothing with what was published and a descriptor it kept reaches only the staged tree, which the next commit reaps. The copy is copy-on-write where the file system does that and a plain copy where it does not.
Reach for copy where the guarantee matters more than the work:
a body that hands a file to something else before committing, or one
whose behaviour after the commit you would rather not have to reason
about.
#:seen-at says where a step sees the content, as it does for a
workspace or a secret. It names the content rather than the directory
holding it, so it has to have a directory above it, and that directory
is what the step is given to write in: a step holding two file outputs
under one directory is refused, as one told to see two resources at
one place is.
(define site (file-output-resource #:destination "/srv/www/site" #:seen-at "/out/site"))
Everything the resource makes lives in one hidden directory beside the destination, which is where it has to be, since neither a rename nor a copy-on-write copy crosses file systems. Each commit reaps what is there but the generation it published, the one it replaced and its own staging directory, so a destination holds two generations and not a history. Two runs publishing to one destination at once is what a limiter is for, as two runs pushing one branch are.
As with a file input, a pipeline naming a place on the machine runs only on a machine that has it. Reach for a file output where a run has to leave something on the machine it ran on and a store item or a step’s output will not do.
(host-network-resource) gives a step the host’s network. A step
that declares it reaches out as the pipeline process could; a step
that does not runs in a network namespace of its own, a private view
of the system’s network holding nothing but a loopback device. Such a
step can still talk to itself over 127.0.0.1, so a test that
starts a server and connects to it works, and it cannot open a
connection to anything else. Attaching returns nothing of use: the
declaration is what does the work, and a step need not attach the
resource to use the network it names.
So the steps that clone, fetch, publish or call an API declare it, and
the steps that build and test do not. A step that builds through the
guix-daemon resource needs no network of its own, because the
Guix daemon downloads on its behalf and runs outside the step.
(define work (workspace-resource #:name 'work)) (define net (host-network-resource)) (define fetched (step (name 'fetch) (inputs (list work net)) (modules '((automate helpers git))) (body #~(git (attach #$work) "fetch" "origin"))))
A step that reaches the network without declaring it fails where it connects, with whatever error its program gives for an unreachable address. The certificates needed to verify a TLS connection are among the variables every step is given, and the files they name are in the step’s file system, so a step that has the network can use it. Where a kernel gives an unprivileged process no namespaces at all, no step can be confined: the run fails, and --unconfined on a one-shot program or a pipeline daemon spawns every step the way they were spawned before Automate confined them.
(limiter-resource #:max N #:scope SCOPE) is a set of N
slots. attach returns when a slot is free and holds it until
release or the end of the step. With #:max 1, the
default, it serialises the steps that hold it.
The scope says how far the limit reaches. process, the default,
is one pipeline process: a pipeline daemon, or a one-shot program.
Every run there of every pipeline listing the limiter takes turns on
it, so two runs at once in one pipeline daemon wait on each other, and
so do runs of two pipelines that list one limiter the definition file
made once. Two limiters declared separately are two, whatever their
names. pumphouse is every pipeline attached to the pumphouse
that declares a limiter of the name #:name gives it, which a
pumphouse limiter must have, so it serialises across pipelines and
across machines. A pumphouse limiter needs the pumphouse to be
reachable: a step blocked on one errors after thirty seconds without it.
A step blocked on a limiter has already started, and its process sits idle. Attach the limiter as late as the body allows and release it as soon as the limited work is done. Late means after the work that has nothing to do with what the limiter covers, not after reading the state the limited work is built on: a body that reads outside the slot and writes inside it has excluded nothing, since what it read may have changed by the time it writes:
(define publish-slot (limiter-resource #:name 'publishing #:max 1 #:scope 'pumphouse)) (define published (step (name 'publish) (inputs (list (step-output built 'dist) publish-slot)) (body #~(let ((dist #$built:dist)) (prepare dist) (attach #$publish-slot) (upload dist) (release #$publish-slot) (tidy dist)))))
A limiter gives a step exclusion, which is not the same as order.
The steps holding it run one at a time, in the order they asked,
and that is the order their runs happened to reach the step rather
than the order the runs were started in: two runs of one pipeline
publishing two commits can publish them oldest last. Nor can a
limiter be made to order them, since a run cannot know whether a
newer run exists or will get as far as publishing. What a step can
do is hold the slot across both the reading and the writing and
stand aside when it finds itself overtaken, which leaves the final
state the newest run’s, whatever order they arrived in.
.automate/pipelines.scm in the source does this: its
publishing step names the branch the site is built from and pushes
only while that branch is still at the commit the run built, which
git-remote-tip in
(automate helpers git) answers.
Two steps that attach two limiters in opposite orders can deadlock each other. Nothing detects this; a step timeout is the only way out.
A step’s program carries a Guix, so that a body can compute
derivations and talk to the Guix daemon. That Guix is a library.
Its package collection is the one Guix’s own package pinned when the
pipeline was lowered. Nobody chose that collection, and it lags
behind guix pull, so it is never where a step’s packages
come from. Packages come from a guix-channels resource:
(define channels (guix-channels-resource #:name 'channels))
By default that is the Guix that lowers the definition file, at the
channels and commits its guix pull profile was made from,
which is what guix describe prints on the machine running
guix build. Lowering copies nothing and builds nothing
for it: the definition refers to the Guix already there, and the
channels are recorded in the structure, so a run shows which
collection its steps built with. A pipeline that pins its own
collection passes #:channels, a list of channel records of
(guix channels), and gets a Guix built from them, which needs
the channels’ checkouts when the file is lowered.
Attaching returns the directory of that Guix. Its bin/guix
is the command, and the helpers in (automate helpers …)
take packages from it once current-guix of (automate
helpers guix) names it, which a body does at its start:
(define fetched (step (name 'fetch) (inputs (list work channels repository)) (modules '((automate helpers git))) (body #~(let ((source (attach #$work))) (current-guix (attach #$channels)) (git source "clone" "--depth=1" #$repository ".") (git-output source "rev-parse" "HEAD")))))
Without that, a helper that needs a package raises no-guix
rather than take one from the library: the git the helpers run, and
anything a body asks build-package for, come from the channels
or not at all. The Guix is opened as an inferior, a guix
repl of it that the step asks for packages, the first time a helper
needs it.
Taking a package from the channels means building it, so such a step
needs a guix-daemon input as well: the Guix daemon’s socket is
in a step’s file system only where the step declares it. That is a great
deal of authority for a step whose need is one program. A body that
wants a program it can name takes it from the Guix that lowers the
definition instead, and holds neither resource:
(current-git #$(file-append git-minimal "/bin/git"))
The program is then the lowering Guix’s rather than the channels’, and it is in the definition’s closure, so the step builds nothing and talks to no Guix daemon. The channels are still what a step wants where the collection has to be the one the pipeline pinned, as the step building the development environment does.
A step that only runs a script in a profile needs no Guix at all when
the profile carries a bash: invoke-in-profile uses the
profile’s own. The profile is the whole of that script’s
PATH, since a step has none of the machine’s, so what the
profile lacks the script cannot run. A profile made from the
development inputs of a package on gnu-build-system carries the
standard tools; one on guile-build-system carries tar, gzip,
bzip2 and xz and no others, so a manifest for one adds coreutils,
grep, sed and whatever else configure and make
reach for.
The channels of a guix-channels resource are fixed when the
definition is lowered. For a pipeline that lowers and installs its
own definition, this means the channels never change: the Guix that
lowers the new definition is the captured one, and it captures
itself again. A pipeline whose channels have to move on takes them
from another pipeline at run time instead (see Run History).
(guix-daemon-resource) is the Guix daemon’s socket, and with it
the whole store: a step holding it can build and add to the store, and
read back anything there. That is a lot of authority, comparable to
unconfined execution, so hold it in the steps that build and not in the
rest. Attaching returns the socket path, taken from
GUIX_DAEMON_SOCKET or Guix’s default, which
open-connection of (guix store) accepts. A confined step
holding the Guix daemon also sees its public signing key,
/etc/guix/signing-key.pub or the one under
GUIX_CONFIGURATION_DIRECTORY, and nothing else of the Guix
daemon’s configuration. The key names the Guix daemon to other
machines: guix deploy reads it to authorize a target to take
what the Guix daemon built.
What a step builds is only a path until something roots it, and the
Guix daemon’s garbage collector may take it between one step and the next.
(store-item GUIX-DAEMON PATH) asks the resource to keep the item:
the resource makes a garbage collector root for it, and answers a
store item, an object the step provides as an output. Call it while
the connection that built the path is still open, so that nothing
can collect the item in between. A later step takes the item as an
input and attaches it, which returns the path. The root
lasts until every step that could use the item has finished.
The example below builds on the cloned step, the work
workspace and the channels resource defined earlier in this
chapter.
(define guix-daemon (guix-daemon-resource)) (define environment (step (name 'environment) (inputs (list cloned work guix-daemon channels)) (modules '((automate helpers profiles))) (body #~(build-development-profile #$guix-daemon #$channels (string-append (attach #$work) "/source/guix.scm"))))) (define built (step (name 'build) (inputs (list environment work)) (modules '((automate helpers profiles))) (body #~(let ((source (string-append (attach #$work) "/source"))) (invoke-in-profile (attach #$environment) source "make")))))
This is what guix shell -D -f guix.scm would assemble, done
inside a step: the Guix of the channels evaluates guix.scm, so
the packages it names are the ones the developers get, the profile is
built through the Guix daemon, kept as a store item, and attached by the
steps that build in it. A run whose steps declare no
guix-daemon has nothing to root with, and a store path passed
around in such a run is a string like any other.
A store item is recorded, and shown on the run’s page, as the list
(store-item PATH). Passed to another run as a parameter,
through trigger-run, that list is adopted by the receiving
run’s own guix-daemon resource: the path is rooted again and
the step gets an item to attach. A run without such a resource gets
the list as it is.
A cache is a directory that outlives the run that wrote it, so a later run can use what an earlier run left there. Put in it what is slow to fetch again, such as clones of git repositories. Declare a cache by name:
(define downloads (cache-resource 'downloads))
Attaching a cache returns a directory. No other step holds that
directory until the step that attached it finishes or calls
release, and while another step holds it, attach waits.
#:max allows more copies. With #:max 2, two steps can
use the cache at once, each in a directory of its own, and each copy
takes its own space. A step finds what an earlier step left only if
it gets the same copy, so a body that reads from a cache has to cope
with an empty one.
The caches are kept in the pipeline process’s cache directory, given
with --cache-directory=DIR to a pipeline daemon or a one-shot
program. Every pipeline the process serves that declares a cache of
the same name shares it. Without the option, the process makes a
temporary directory and removes it when it stops, so nothing is kept
from one one-shot run to the next. A pipeline shepherd gives each
pipeline daemon a cache directory of its own (see Running a Pumphouse).
The process takes its cache directory to be its own. Nothing stops two processes given the same directory from writing the same copy at once, and the copy that results may be broken, so give each pipeline daemon its own. A pipeline daemon replaced through the Shepherd is not a second user: the old process exits before the new one starts.
A step sees a cache where it is on the machine, and a cache takes no
#:seen-at. Guix needs this: it keeps garbage collector roots
in its cache as symbolic links, which the Guix daemon follows by the
path they were made at.
(guix-channels-cache-resource) is a cache for what Guix keeps
of channels: their clones, which of their commits were authenticated,
and the Guixes built from them. build-guix and
latest-channels take it as #:cache, attach it
themselves and release it when they are done (see Run History).
(shepherd-control-resource #:client SECRET) is a shepherd client:
an object reaching some services of a Shepherd, the service manager of
Guix System, with some actions: asking their status, starting, stopping
and restarting them, and loading new definitions of them. A step is
never given the Shepherd’s socket. Whoever holds that socket can
evaluate code as the Shepherd’s user, root for the system Shepherd, so
the socket stays with a shepherd bridge. The bridge is a process of
Automate’s that holds the socket and offers steps a part of what
herd does. It runs as a service of the Shepherd it bridges,
and its script declares the clients it publishes, each reaching some
services with some actions on them. At start it writes each client,
its socket and the client’s sturdyref, to a file in the secrets
directory of the pipeline daemon that is to hold it, so whoever can
read the file holds the client. The resource is given that file as a
secret-resource; the pipeline process reads it afresh for each
run, and the step is given a handle that passes its messages on, never
the secret itself, so the step does not declare the secret among its
inputs.
A step attaches the resource and passes what attach returns
to shepherd-status, shepherd-start,
shepherd-stop, shepherd-restart or
shepherd-load of (automate
pipeline step). A service the client does not reach, or an action it
does not offer, is refused, and the step errors. shepherd-load
takes a file in the store only. Loading a file runs its code as the
Shepherd’s user, and a store file cannot change once built, so what the
Shepherd loads is what the step built and nothing edited after.
This is how a run that reconfigured the system restarts the services
the reconfigure changed. guix system reconfigure loads the
new definition of every running service into the Shepherd and
restarts none of them, so a changed nginx configuration is not read
until something restarts nginx. A client reaching nginx and
offering restart lets the step that reconfigured do that, and
nothing more.
(define web (shepherd-control-resource #:name 'web #:client (secret-resource 'web))) (define reconfigured (step (name 'reconfigure) (inputs (list (host-network-resource))) (body #~(invoke "guix" "system" "reconfigure" "/etc/config.scm")))) (define restarted (step (name 'restart-web) (inputs (list reconfigured web)) (body #~(shepherd-restart (attach #$web) 'nginx))))
The bridge’s script calls run-shepherd-bridge of
(automate shepherd bridge) with a
shepherd-client for each client it publishes, and
examples/shepherd-bridge.scm is one. It runs as root, as a
service of the system Shepherd started once the Shepherd’s socket
exists, and keeps its identity in a state directory, so that the
clients’ files stay valid across its restarts. A client offering
register is for the Shepherd that runs the pipeline daemons,
which has a bridge of its own (see Pipelines).
(gate-resource NAME #:needed N) is a decision made from outside
the run. Attaching it waits until N distinct parties have
approved it with automate gate approve RUN-ID NAME, and then
returns. If someone rejects it instead, with automate gate
reject, attach raises and the step errors unless the body
handles the exception. automate gate list RUN-ID lists a run’s
open gates, each with how many parties it needs and who has approved so
far.
(define approval (gate-resource 'approve-release #:needed 2)) (define released (step (name 'release) (inputs (list (step-output built 'dist) approval)) (timeout (seconds-for #:days 1)) (body #~(begin (attach #$approval) (publish #$built:dist)))))
A step waiting on a gate holds its process open for as long as the decision takes. Give such a step a timeout, which bounds the wait. Gates need a pumphouse: the approval travels from the command line through the pumphouse to the run. Attaching the same gate from several steps shares one decision.
(pipeline-trigger-resource #:pipeline PIPELINE) is a live
reference to another pipeline attached to the same pumphouse. A step
attaches it and calls trigger-run to start a run of that
pipeline, getting the new run’s id back. The child run records this run
as its cause, which its page shows.
(define deploy (pipeline-trigger-resource #:name 'deploy #:pipeline 'site/deploy)) (define handed-over (step (name 'hand-over) (inputs (list (step-output built 'version) deploy)) (body #~(let ((version #$built:version)) (trigger-run (attach #$deploy) #:parameters `((version . ,version)))))))
A pipeline cannot trigger itself, directly or through a chain of triggers, and a chain has a maximum depth. The one exception is a run of another definition of itself, below.
The request waits for a pipeline process serving the pipeline to take
it, as long as the pumphouse’s #:dispatch-wait says, three
seconds by default, or as long as #:wait says, up to ten times
that. That covers a pipeline whose daemon is restarting on a new
definition, which leaves it without a process for a few seconds.
#:definition names a definition of the pipeline by the store
path of its definition directory, and the request then waits for a
process serving that definition. This is how a pipeline that lowers
and installs definitions, its own among them, has a run of what it
installed once the pipeline daemon has come back on it.
program-definition of (automate helpers definitions)
answers the path for a pipeline of a program lower-definition
lowered. A pipeline may start itself this way, by naming a definition
other than the one the run asking is of; a request naming the same
definition is refused as a cycle, as is a chain that comes back to a
definition already on it.
(define trunk (pipeline-trigger-resource #:name 'trunk #:pipeline 'project/trunk)) (define rerun (step (name 'rerun) (inputs (list lowered installed trunk repository ref commit)) (modules '((automate helpers definitions))) (body #~(if (memq #$installed '(started loaded)) (trigger-run (attach #$trunk) #:parameters `((repository . ,#$repository) (ref . ,#$ref) (commit . ,#$commit)) #:definition (program-definition #$lowered 'project/trunk) #:wait 60) 'unchanged))))
The run asking records that its request waits, and that it was dropped when the wait passed with no process serving the definition attached, since no run of the other pipeline exists until a process takes one on.
(param NAME) declares something a run is given when it is
started: a branch, a tag, a repository URL. A step lists the
parameter among its inputs and reads its value by splicing it into
the body with #$, as it does any other input.
A parameter’s value may be any plain data: a string, a number, a
symbol, a list. A run started by a webhook gets what the delivery
carried (see Forge Webhooks). A run started by trigger-run
gets what the triggering step passed.
A run has to be given every parameter that any step of the pipeline
takes as an input. This holds even for a parameter that only a step
the run will skip takes. A run lacking one is refused before any
step starts, with a missing-parameter error naming it. A
parameter has no default value, so a pipeline that can run without
a value takes one that says so, such as an empty string, and its
steps check for it.
On the command line, --param=NAME=DATUM reads the value as one
Scheme datum, and --param-string=NAME=STRING takes the text as
it is. Most values a pipeline is given are strings, such as a
repository URL or a ref, and those need --param-string. Read
as Scheme, ‘refs/heads/main’ is a symbol and ‘1.10’ is the
number 1.1, so a body expecting a string would raise, or would get the
wrong value.
$ automate run start project/trunk \
--param-string=ref=refs/heads/main --param=jobs=4 \
--param='targets=("x86_64-linux" "aarch64-linux")'
A value is compared by how it is written, so the string
"12" and the number 12 are two different values. This
matters when runs are listed by their parameters (see Run History): a run started with --param-string=number=12 is not
one of the runs a Forgejo webhook started for pull request 12, whose
number is the number 12.
Parameters cannot change the structure of a pipeline. The set of steps and the edges between them are fixed when the file is built; what a parameter can do is change what a step does with them.
Parameters are recorded on the run and shown on its page, so they carry no secrets. A token goes in a secret file (see Resources) and a parameter says which repository to use it for.
A step can list the runs of a pipeline that the pumphouse has recorded. This is how a run compares itself with earlier runs of the same thing. For example, a step can announce that the tests of a pull request have started failing or are fixed, and stay quiet when nothing changed.
The runs come from a run-history resource. A step attaches it
and passes it to list-runs:
(define history (run-history-resource)) (define number (param 'number)) (define checked (step (name 'check) (outputs '(status)) (body #~(begin (run-tests) (provide 'status 'passed))))) (define checks (step (name 'checks) (inputs (list (step-output checked 'status #:required? #f))) (body #~(if (input-present? #$checked:status) 'passed 'failed)))) (define previous (step (name 'previous) (inputs (list checks history number)) (body #~(match (list-runs (attach #$history) #:matching '(number) #:before (run-id) #:provided '(checks) #:outputs '(checks) #:limit 1) ((run) (listed-run-output run 'checks)) (() #f)))))
previous answers what checks provided in the newest
earlier run of the same pull request, or #f for the first one.
A run is listed only when it matches every keyword given. The common
ones are these:
#:matching names parameters of this run, and keeps the runs
given the same values. #:parameters gives the values itself,
as an alist, which is how a pull request’s run finds the runs of the
branch it targets.
#:before and #:after take a run id and keep the runs
requested before or after it. (run-id) is this run’s.
#:provided keeps the runs in which each of the listed step
outputs was provided. A step’s name stands for its value
output, and a (STEP OUTPUT) list for another.
#:outcome keeps the finished runs that ended in one of the
listed outcomes, such as '(succeeded).
#:finished keeps the runs that have finished when it is
#t, and those still waiting or going when it is #f.
#:outputs says which recorded output values to return with each
run, and listed-run-output reads one of them.
#:order, #:direction and #:limit say which runs
come first and how many are returned. The default is newest request
first, with no limit.
Each run is an alist of its id, pipeline,
parameters, outcome, the times it was requested,
started and finished, and its outputs.
(run-history-resource) lists the runs of the pipeline that
declares it. #:pipeline names another pipeline:
(define trunk-history (run-history-resource #:pipeline 'project/trunk))
A run may list the runs of its own pipeline and of the pipelines its
run-history resources name, and no others. The pumphouse reads
the list from the definition, as it does for pipeline triggers.
Runs overlap. The newest earlier run may still be going, and its
steps may not have got as far as the one you want to compare with.
Use #:provided or #:outcome to keep only the runs that
have an answer. Without either, #:limit 1 can return a run
that has none.
A run can finish after a run that was requested later. This happens
when a pull request gains a commit while the previous commit is still
being checked. Before announcing a change, the older run can ask for
runs #:after (run-id) that already provided their value. If
there is one, the newer run has made the comparison and the older run
stays quiet.
Parameters match by how their values are written, so the string
"12" does not match the number 12 (see Parameters).
A run that lacks a parameter the query names is never listed.
#:matching raises when this run lacks the parameter.
Only an output whose value is plain data is recorded.
#:outputs returns nothing for an output whose value is a store
item. Put the value to compare in a small output of its own, as
checks is above.
The runs are the pumphouse’s records. In a one-shot run without a
pumphouse, the step can still attach the resource, but
list-runs raises.
One pipeline can keep a set of Guix channels up to date, and other pipelines can build their Guix from it. Each run of the first pipeline is an update, with its log, and going back to earlier channels means reading an earlier run.
The first pipeline’s step finds the latest commits with
latest-channels of (automate helpers guix) and returns
them as data, the code channel->code of (guix channels)
writes. It starts from the channels its own last successful run
returned. The commits are authenticated, as guix pull does,
and a commit that does not descend from the previous one is refused.
(define guix-daemon (guix-daemon-resource)) (define net (host-network-resource)) (define guix-cache (guix-channels-cache-resource)) (define own-history (run-history-resource)) (define updated (step (name 'update) (inputs (list own-history guix-daemon net guix-cache)) (modules '((automate helpers guix) (guix channels) (ice-9 match))) (body #~(let ((previous (list-runs (attach #$own-history) #:outcome '(succeeded) #:outputs '(update) #:limit 1))) (latest-channels #$guix-daemon %default-channels #:current (match previous ((run) (listed-run-output run 'update)) (() '())) #:cache #$guix-cache)))))
What else that pipeline does before it succeeds is up to its author: it might build the Guix, or test something with it. A run that fails is not read by a step that asks for successful runs only, so a check there keeps a bad update from the other pipelines.
A pipeline that follows it reads the channels, builds that Guix with
build-guix, and returns it as a store item. Later steps set
current-guix to what attaching the item returns:
(define channels-history (run-history-resource #:name 'channels-history #:pipeline 'guix/channels)) (define guix (step (name 'guix) (inputs (list channels-history guix-daemon net guix-cache)) (outputs '(value channels)) (modules '((automate helpers guix))) (body #~(let* ((latest (car (list-runs (attach #$channels-history) #:outcome '(succeeded) #:outputs '(update) #:limit 1))) (channels (listed-run-output latest 'update))) (provide 'channels channels) (build-guix #$guix-daemon channels #:cache #$guix-cache))))) (define hello (step (name 'hello) (inputs (list guix guix-daemon)) (modules '((automate helpers guix))) (body #~(begin (current-guix (attach #$guix)) (build-package "hello")))))
build-guix builds with the Guix library in the step’s program,
as guix time-machine does. This takes no package from the
library’s collection: each channel is built by its own build
procedure. The channels output records which channels the run
used, and the run’s page shows it. Which run to read is the
pipeline’s choice. A pipeline that tests against several Guixes can
read several pipelines, one for each set of channels.
Guix needs a clone of each channel to build it, even when the Guix it
builds is already in the store. Without a guix-channels
cache, every run clones every channel again, and the clone of Guix
alone is hundreds of megabytes. Declare the cache and give the
pipeline daemon --cache-directory.
A pipeline that lowers its own definition does so with this Guix:
lower-definition with current-guix set to it. The new
definition then captures these channels, and its steps’ Guix library
comes from them.
Reading another pipeline’s runs needs a pumphouse. A one-shot program
reaches one with --pumphouse. Until the first pipeline has a
successful run, the step above has no run to read, and it errors.
A step can abort other runs. The usual reason is that they are out of date. When a pull request gains a commit, the run checking the previous commit is wasted work, and its result will be replaced anyway. The newer run can abort it.
A step aborts a run through a run-control resource. It finds
the runs to abort through a run-history resource
(see Run History), then passes each id to abort-run:
(define history (run-history-resource)) (define control (run-control-resource)) (define number (param 'number)) (define supersede (step (name 'supersede) (inputs (list history control number)) (body #~(let ((control (attach #$control))) (for-each (lambda (run) (abort-run control (assq-ref run 'id) #:reason "superseded")) (list-runs (attach #$history) #:matching '(number) #:before (run-id) #:finished #f))))))
supersede aborts every unfinished run of the same pull request
that was requested before this one. #:before (run-id) matters.
Without it, two runs requested close together could each abort the
other.
Aborting a run kills its running steps and skips its pending ones.
The run finishes as errored. Its record shows the reason, and
which pipeline and run asked for the abort.
abort-run answers aborting, or finished when the
run had already finished. A run can finish between the step listing
it and the step aborting it, so a finished run is not an error.
(run-control-resource) aborts runs of the pipeline that declares
it. #:pipeline names another pipeline:
(define deploy-control (run-control-resource #:pipeline 'site/deploy))
A run may abort the runs of its own pipeline and of the pipelines its
run-control resources name, and no others. The pumphouse reads
the list from the definition, as it does for pipeline triggers.
abort-run raises a not-permitted error for a run of any
other pipeline.
A run may not abort itself, and abort-run raises a
not-permitted error if it tries. Aborting kills every running
step, including the step that asked. To end its own run, a step
fails.
The steps of an aborted run are killed with SIGKILL. They get
no chance to clean up, so a step that was part way through publishing
something leaves it part way. If a pipeline publishes, abort its runs
only from a step that runs before any publishing could start in them,
or keep publishing in a pipeline of its own that nothing aborts.
Aborting a run does not abort the runs it started. A run started by a step that calls a pipeline carries on, because another run may be waiting on it too.
Aborting goes through the pumphouse. In a one-shot run without a
pumphouse, the step can still attach the resource, but
abort-run raises.
A pipeline that runs on every push or pull request should say something when its result changes: the tests started failing, or they pass again. Saying something on every run is noise that people learn to ignore. (automate helpers notify) builds this from three parts, each a step or a call in one:
summarise-checks. Its value is recorded, so later runs can read
it.
checks-change, which reads the earlier runs through a
run-history resource (see Run History). The answer is a
transition: first, broken, fixed,
still-passing, still-failing, changed-failure or
superseded.
send-email, post-json for a chat service’s web hook, or
a comment on the pull request.
(define history (run-history-resource)) (define net (host-network-resource)) (define token (secret-resource 'forge-token)) (define repository (param 'repository)) (define number (param 'number)) (define built (step (name 'build) (body #~(begin (run-build) 'built)))) (define checked (step (name 'check) (inputs (list built)) (body #~(begin (run-tests) 'ok)))) (define checks (step (name 'checks) (inputs (list (step-output built 'value #:required? #f) (step-output checked 'value #:required? #f))) (modules '((automate helpers notify))) (body #~(summarise-checks `((build . ,(input-present? #$built)) (check . ,(input-present? #$checked))))))) (define notified (step (name 'notify) (inputs (list checks history net token repository number)) (error-permitted? #t) (modules '((automate helpers notify) (automate helpers secrets) (automate forge forgejo))) (body #~(let ((transition (checks-change (attach #$history) #$checks #:matching '(repository number))) (token (read-secret (attach #$token)))) (unless (eq? transition 'superseded) (forgejo-sticky-comment #$repository token #$number (format #f "Checks ~a~a" (checks-state #$checks) (if (run-url) (string-append ": " (run-url)) "")) #:marker "checks")) (when (memq transition '(broken fixed changed-failure)) (forgejo-comment #$repository token #$number (format #f "The checks are ~a." transition))) transition))))
The sticky comment is one comment on the pull request, edited by each run, so the pull request always says where it stands. Editing a comment notifies nobody. The second comment is new, so the forge tells everyone watching, and it is made only when the result changed.
A pipeline for a branch compares the runs of that branch, with
#:matching '(repository ref), and sends mail or posts to chat
instead of commenting. send-email runs msmtp from the
Guix a guix-channels resource attaches, so the step sets
current-guix first (see Resources). msmtp’s settings,
including the server’s password, go in a secret file, which msmtp
refuses unless only its owner can read it.
(define channels (guix-channels-resource)) (define mail-settings (secret-resource 'mail-settings)) (define ref (param 'ref)) (define mailed (step (name 'mail) (inputs (list checks history net channels mail-settings repository ref)) (error-permitted? #t) (modules '((automate helpers notify))) (body #~(let ((transition (checks-change (attach #$history) #$checks #:matching '(repository ref)))) (current-guix (attach #$channels)) (when (memq transition '(broken fixed changed-failure)) (send-email #:to "team@example.org" #:subject (format #f "trunk is ~a" transition) #:body (format #f "~a~%" (run-url)) #:configuration (attach #$mail-settings))) transition))))
The checks step and the notify step must run when the checks fail. Give them the checks as inputs that are not required (see Outputs and Errors); a required input that is never provided skips the step, and nobody hears about the failure.
Mark the notify step error-permitted?. A mail server that is
down should not turn a passing run into a failed one.
A run that is aborted, or whose pipeline process dies, never reaches its notify step and records no summary. The next run compares itself with the last run that did, so a change is reported late, not lost.
superseded means a run requested later has already recorded its
summary, which happens when a pull request gains a commit while the
previous commit is still being checked. The later run does the
comparing, so a run that is superseded tells nobody, and does not
update the sticky comment with an older result.
The notify step needs the network, so it lists a host-network
resource.
(pipeline NAME #:steps STEPS) gathers steps into a pipeline
named by a symbol. A name with slashes, such as
site/deploy, is grouped by its prefix on the front page of
the web interface. collect-steps walks the inputs of the
steps it is given and returns every step reachable from them, so a
pipeline is usually declared from its final steps:
(pipeline 'site/deploy #:steps (collect-steps rolled-back notified))
The pipeline is checked as it is made: no two steps with one name, no cycle, every input selecting an output its source has, every step input among the steps given. A problem is reported with the step and the name concerned.
A step’s outputs belong to the pipeline’s inside. Which step
provided a value, and what it called it, is a detail the author
changes freely. #:outputs names what the pipeline as a whole
produced, so that whoever reads a finished run need not know the
steps at all:
(pipeline 'site/deploy #:steps (collect-steps notified) #:outputs `((url . ,(step-output deployed 'url)) (report . ,report)))
An output selects a step’s output the way an input does, so a step
given plainly stands for its value output. A one-shot
program prints these when the run ends, and a pumphouse records them
against the run.
Name the few values someone outside the run needs and no more. The
names are what the pipeline is read by, so keep them still while the
steps beneath them move: a step renamed, split in two or replaced
leaves the outputs as they were. error is not a name an
output may take, since it is what a step’s own failure is called
(see Outputs and Errors).
An output whose step never provided it is absent, not false. A step
that was skipped provides nothing, so a pipeline whose deployment
only some runs reach answers without its url on the runs that
stopped short. Read the result with that in mind: a missing name
means the run did not get that far, which is not the same as a step
that ran and produced #f.
A step usually runs a body. It can instead run another pipeline, and take what that pipeline answers with as its own outputs:
(define package (param 'package)) (define plan (step (name 'plan) (inputs (list package)) (body #~(provide 'value `((package . ,#$package)))))) (define built (step (name 'built) (inputs (list plan)) (outputs '(derivation)) (calls (call-pipeline 'guix/build-package #:parameters plan))))
Such a step is no process. The pipeline process starts the run, waits
for it, and ends the step when it ends: succeeded where that run
succeeded, and errored otherwise, with an error output naming the run
and what became of it. A step calling a pipeline does not count against
--cap, which bounds the step processes on this machine and not
what other machines are doing.
The step provides each output it declares, taken from the output of that
name the called pipeline answers with, as its #:outputs names
them. It provides
run besides, the id of the run it started, and provides it as
soon as the run starts rather than when it ends. A step waiting only on
run therefore runs while the called run is still going, which is
how a build is reported as it begins.
#:parameters says what the run is given. A call computes
nothing: it takes either an alist fixed when the pipeline is built, or a
step whose value is such an alist, worked out by an ordinary body. Put
the computing in a body upstream, as plan does above, and name
that step. It has to be among the calling step’s inputs, so that the
call waits for it.
The pipeline called is named, not built in. What runs is whatever is attached under that name when the call is made, which is what lets the two pipelines be built, reviewed and deployed apart, and lets the called one run on another machine. The cost is that the calling pipeline’s definition no longer covers everything that ran: the run page links the two runs, and that link is what says which definition answered.
Calling needs a pumphouse. A pipeline’s name means nothing without the registry that holds it, so a one-shot program run with no pumphouse errors such a step rather than guessing.
A pipeline may start the pipelines it declares and no others. The
pumphouse works that out from the structure a pipeline process
registers: every step that calls a pipeline, and every
pipeline-trigger resource, names a target, and those are what a
run of it may start. There is nothing to configure, and a pipeline
cannot be given a reach its own definition does not show. A run asking
for anything else is refused.
Nothing else can check a call. The called pipeline is named rather than built in, so it is not in the caller’s closure and the build cannot see it. The pumphouse holds both structures, so the pipeline’s page says what is unsettled: a called pipeline it does not know, and an output a step takes that the called pipeline does not answer with. It says so and refuses nothing. A called pipeline that has not registered yet is the ordinary state of affairs while a system is being brought up, and the caller is right either way once it has.
A pipeline’s steps are fixed when it is built. There is no making a
step per package that changed, per pull request open, or per test to
bisect, because none of those is known then. A run can make a run
apiece instead: #:each takes a step whose value is a list of
parameter alists and calls the pipeline once for each of them.
(define packages (param 'packages)) (define plans (step (name 'plans) (inputs (list packages)) (body #~(provide 'value (map (lambda (package) `((package . ,package))) (string-split #$packages #\,)))))) (define built (step (name 'built) (inputs (list plans)) (calls (call-pipeline 'guix/build-package #:each plans #:width 8))))
Such a step declares no outputs. It provides runs, the ids of
the runs it started in the order of the list, and results, one
entry per element in that same order:
((run . "a17c93ff21") (outcome . succeeded) (outputs . ((derivation . "/gnu/store/..."))))
#:width bounds how many of those runs are going at once, the next
starting as one ends. Without it they all start together, which is
fine for a handful and unkind to a pumphouse for five hundred.
The step errors where its runs did not all succeed. Its results
still say what each run did, so the ones that worked are not lost with
the one that did not: a step downstream can read them and report, and
error-permitted? on the calling step is what decides whether the
run it is part of fails. An empty list starts no run at all, and the
step succeeds with empty runs and results, which is worth
knowing when the list comes from a search that found nothing.
The file’s last expression says how the pipeline runs. (one-shot
PIPELINE) makes a program that runs it once, with its parameters on the
command line, as Getting Started shows.
(run-pipeline-daemon (list PIPELINE …)) makes a program
that attaches the pipelines to a pumphouse and serves runs of them until
stopped:
(run-pipeline-daemon (list website pull-request))
$ $(guix build -f pipelines.scm) \
--pumphouse=/var/run/guile-automate/any \
--secrets=/etc/automate/secrets
The pipeline daemon takes --pumphouse=STURDYREF, or reads
AUTOMATE_PUMPHOUSE: the sturdyref of a registration that covers
its pipelines, or a file holding it (see Running a Pumphouse). It
takes --unix-socket=FILE for a pumphouse on the same machine, and
the same --secrets, --unconfined,
--pass-environment and --expose as a one-shot program.
The pipeline daemon reads the file again each time it tries to connect:
every second until it attaches, and again after it loses the pumphouse.
So it can be started before the pumphouse has written the file.
--give-up-after=SECONDS makes it exit once it has gone that long
without being attached; by default it keeps trying. An attempt that has
neither attached nor failed within --connect-timeout=SECONDS,
thirty by default, is abandoned and another made.
A pipeline daemon sent SIGTERM, or interrupted, stops taking runs and
lets the runs it has finish. The pumphouse gives new runs to another
pipeline daemon serving the pipeline, or refuses them as busy. Runs
still going after --stop-timeout=SECONDS, sixty by default, are
aborted with the reason pipeline daemon stopping. The pipeline daemon
then sends what is left of their logs, tells the pumphouse it has gone,
and exits. A second signal makes it exit at once, leaving its runs to
be found abandoned. A service manager has to allow for all of this
before it kills the pipeline daemon: the Shepherd waits five seconds
after SIGTERM unless the service’s #:grace-period says otherwise,
so give it --stop-timeout and half a minute more.
Under the Shepherd, though, a signal is not this clean stop. The
Shepherd stops a service by signalling its whole process group, and the
pipeline daemon’s step processes are in that group, so herd
stop ends every running step at the same moment as the pipeline daemon,
which then finds its runs already dead. When the runs matter, stop the
pipeline daemon from a run instead, or wait until it has none.
A run stops its own pipeline daemon through a process-control
resource. A step attaches it and calls drain-and-stop with a
reason. The pipeline daemon takes no new run from then on, and exits
with status 0 once every run it has, that one among them, has finished,
however long they take; a bound on how long is the pipeline’s to put,
with a step timeout. The run records a drain-and-stop event, and
until the pipeline daemon has gone the pipeline’s page shows it
stopping, with the reason, as does the page of each run still keeping
it. This is how a pipeline that reconfigures the system its pipeline
daemon is a service of brings the pipeline daemon back on the new
definition. guix system reconfigure loads the new definition
of a running service without restarting it, so a last step calls
drain-and-stop, the pipeline daemon leaves when the run is done,
and a service with #:respawn? #t is started again by the Shepherd
on the definition it now has.
A process-control resource given #:pipeline stops the
pipeline daemons of that pipeline instead, through the pumphouse: every
pipeline daemon attached under the name, or the one #:instance
names. A pipeline stops the pipeline daemons of no pipeline it does not
name this way. This is how a run that deployed new code to several
machines brings each machine’s pipeline daemon back on it, one instance
at a time. An operator does the same with automate pipeline
drain-and-stop NAME. A pipeline that installs pipeline daemons asks
with #:required? #f, since on its first run, started with
--once, there is no pipeline daemon yet to drain; it gets the
empty list instead of an error.
(define reconfigured (step (name 'reconfigure) (inputs (list (host-network-resource))) (body #~(invoke "guix" "system" "reconfigure" "/etc/config.scm")))) (define control (process-control-resource)) (define restarted (step (name 'restart) (inputs (list reconfigured control)) (body #~(drain-and-stop (attach #$control) #:reason "system reconfigured"))))
--output=DIR is where each step’s log is written before it is
uploaded to the pumphouse; a temporary directory by default.
--working-directory=DIR keeps both the logs and the runs’
workspaces in DIR instead, as Disk space below describes.
--cache-directory=DIR is where the caches its pipelines declare
are kept between runs and across restarts (see Resources).
--cap=N bounds the number of step processes running at once,
derived from the CPU count by default, which is an operator’s safety
valve rather than a limiter. --instance=NAME, or
AUTOMATE_INSTANCE, is the name the pipeline daemon attaches
under, the host’s name by default, and --max-runs=N bounds the
runs it takes on at once, which nothing does by default; both are for
several pipeline daemons serving one pipeline (see Running a Pumphouse). Runs are started at the pumphouse: by the command line, a
timer, another pipeline or a webhook. --once NAME makes the same
program run its pipeline NAME once instead, as a one-shot program
does with the rest of its arguments, and exit with the run’s status.
That is how a pipeline that installs the pipeline daemons is first run,
when there is no pipeline daemon yet to serve it, and run again by hand
when the pipeline daemon serving it is broken. --list-definitions
makes it print a line per pipeline it defines, the pipeline’s name and
the store path of its definition, which is what a run request names
with #:definition (see Resources).
The steps are the same programs either way. Building the file with
guix build gives a store path, and that path identifies the
definition: the structure, every step’s program and everything they
depend on. A pumphouse numbers the definitions it sees of each
pipeline, and a pipeline daemon started from a rebuilt file registers a
new one, which applies to runs started after it. Runs under way keep
the definition they started with. Since Automate’s own modules are
copied into each program, upgrading Automate means rebuilding the
pipelines.
A pipeline is itself something Guix can lower: #$PIPELINE in
a G-expression is the definition’s directory, holding the structure
and every step’s program. A program of the author’s own can be
built around a pipeline that way, as one-shot and
run-pipeline-daemon are.
A pipeline daemon is a program in the store, and something has to start
it, start it again when it exits, and start it on a new program when the
pipeline file changes. Automate provides a Shepherd for that, the
pipeline shepherd. run-pipeline-shepherd of
(automate shepherd
pipeline-shepherd) becomes one. It keeps everything under one state
directory: its socket, its own log, a service file for each pipeline
daemon registered with it, the pipeline daemons’ logs, and a shepherd
bridge over itself (see Resources). Its script declares nothing
about the pipeline daemons. A pipeline registers its pipeline daemon
through the bridge, and the Shepherd starts the pipeline daemon, starts
it again when it exits, and loads every registered pipeline daemon’s
service file again at its own next start.
What the script declares is a layout. Every pipeline daemon has a
directory of its own, named after its service, under the shepherd’s
secrets and work directories, and is started on them.
The Shepherd starts a pipeline daemon with no environment to speak of,
so everything it needs is an argument, and the shepherd derives those
from the name: its secrets directory, its working directory, and its
registration, the file pumphouse in the first, which the
pumphouse writes there (see Running a Pumphouse). Whatever else
its steps need, such as a forge token, the operator puts in the same
directory. What every pipeline daemon on the machine needs alike, such
as --unix-socket for a pumphouse not on the default socket, the
script gives as #:arguments. The script’s
#:secrets says what the shepherd itself writes into those
directories: each a shepherd-client of
(automate shepherd bridge) reaching some
service names, written into the directory of the pipeline daemon that is
to hold it.
(use-modules (automate shepherd bridge) (automate shepherd pipeline-shepherd)) (run-pipeline-shepherd #:state-directory "/var/lib/guile-automate/pipeline-shepherd" #:secrets `(("site/shepherd-client" . ,(shepherd-client (services '(site)) (actions '(status register))))))
Running the script in a terminal is enough to try it, and herd
-s DIR/shepherd.sock status shows what it runs, with DIR the
state directory. examples/pipeline-shepherd.scm is one for the
pumphouse of examples/pumphouse.scm. Stopping the Shepherd stops
the pipeline daemons and the bridge, so run it under a service manager
where the pipeline daemons are to stay up.
With a pipeline shepherd running, a pipeline can bring its own pipeline
daemon to the commit it tested, as Automate’s own trunk pipeline does
(see Real Pipelines). Two steps at the end of the pipeline do it.
The first lowers the pipeline file from the checkout with
lower-definition of (automate
helpers definitions). That runs guix build on the file
through the Guix daemon, with the Guix a guix-channels resource
captured, and answers the store path of the program. Which Automate the
file is lowered with, and so the pipeline daemon runs on, is the
#:automate argument: by default the guile-automate package
of that Guix, which a channel has to provide until the package is in
Guix itself. Automate’s repository is such a channel. Its trunk
pipeline builds the package from the channel at the commit under test,
with build-channel-package of (automate helpers guix). It passes that package as #:automate.
The package’s build runs the test suite, so the pipeline daemon runs on
a package that passed it. The second step
waits for the tests, so that a definition whose suite fails is never
registered, and registers the program under the pipeline daemon’s
service name with register-service. It does so through a
shepherd-control resource given the secret the bridge wrote the
client to, in the pipeline daemon’s secrets directory.
register-service answers one of three things. With
started, no pipeline daemon was running, and the Shepherd has
started one on the program. With loaded, a pipeline daemon is
running, and the definition loaded is its replacement. That pipeline
daemon is the one running this very run, so the step asks it to drain
and stop with a process-control resource naming the pipeline, and
the Shepherd starts it again on the new program once the run is done.
With unchanged, the service already runs this program with these
arguments, and the pipeline daemon is left alone. A pipeline daemon
that kept dying is a service the Shepherd has disabled, and a disabled
service is not started when asked. Registering enables the service
before starting it, so a run of the trunk pipeline, from a pipeline
daemon started by hand while the registered one is down, brings the
registered one back on the program it lowered. A pipeline daemon the
Shepherd could not start fails the step rather than answering
started. The arguments are
what the pipeline passed, kept from its first registration, followed by
what the pipeline shepherd’s layout gives every pipeline daemon, which
is composed afresh each time, so a pipeline daemon registered before the
layout gained an option is replaced with one given it. A program is a store
item named by its content, so a commit that touches only the manual, the
tests or NEWS lowers to the same item as the commit before it.
The example builds on the cloned, checked and work
definitions earlier in this chapter.
(define channels (guix-channels-resource)) (define guix-daemon (guix-daemon-resource)) (define shepherd (shepherd-control-resource #:name 'shepherd #:client (secret-resource 'shepherd-client))) (define process (process-control-resource #:pipeline 'site/trunk)) (define lowered (step (name 'lower) (inputs (list cloned work guix-daemon channels)) (modules '((automate helpers definitions))) (body #~(let ((source (string-append (attach #$work) "/source"))) (current-guix (attach #$channels)) (lower-definition (string-append source "/.automate/pipelines.scm") #:load-path (list source)))))) (define registered (step (name 'register) (inputs (list lowered shepherd process checked)) (body #~(let ((result (register-service (attach #$shepherd) 'site #$lowered))) (when (eq? result 'loaded) (drain-and-stop (attach #$process) #:reason "registered from trunk" #:required? #f)) result))))
The first time, no pipeline daemon serves the pipeline, so the operator
runs it once from the built program with --once, on the pipeline
daemon’s own secrets directory and the registration there. That run
lowers the file and registers the pipeline daemon, which the Shepherd
starts on the arguments the layout gives it:
$ D=/var/lib/guile-automate/pipeline-shepherd/secrets/site
$ $(guix build -f pipelines.scm) --once site/trunk \
--pumphouse=$D/pumphouse --secrets=$D \
--param-string=repository=URL --param-string=ref=refs/heads/trunk
The same command is the repair when a change has broken the pipeline daemon: run by hand, it registers a program for the pipeline daemon to come back on.
A pipeline’s code is not read from the repository the pipeline builds. The file is built into a definition beforehand, and a pipeline daemon registers what was built. A run is then given the repository, the reference and the commit as parameters, and its steps fetch that commit themselves (see Forge Webhooks). The file may live anywhere: in the repository it builds, in another repository, or beside the pumphouse’s configuration. Automate keeps its own under .automate/, beside the code they check, but nothing reads them from there. Three things follow.
A commit cannot change the steps that run on it. The structure and each step’s inputs are fixed in the definition the run started with. So are the resources each step holds, and the secrets it can attach. A pull request that edits the pipeline file edits a file in the checkout, and a step that reads that file reads it as text, like any other file there.
What a commit does change is what those steps do. A step that runs
make check over the checkout runs the code in the checkout.
So the definition bounds what a run can reach, and the commit decides
what happens within those bounds. A pipeline checking pull requests
holds the author’s secrets and runs a contributor’s code, so divide
the steps along that line. Automate’s own pipelines declare the
forge token and the network in the steps that fetch, publish and
report, and the steps that build and test the checkout hold neither.
Changing a pipeline takes a rebuild. A commit to the pipeline file changes nothing by itself. Build the file again and start the pipeline daemon from what was built, or have the pipeline register what it built, as above. The new definition applies to runs started after it, and runs under way keep the one they started with.
A pipeline that takes its repository as a parameter runs against any clone of it. That is how one is tried before a forge is told about it, as Real Pipelines shows.
#:web-ui-options is an alist the web interface reads and
nothing else interprets. It is part of the definition. The one key
so far is front-page: with latest-run, the front page
draws the pipeline with the latest run’s statuses on it, which suits
a pipeline whose state is its latest run, such as one that publishes
a site. By default the drawing is plain and the recent runs are
listed beneath it, which suits a pipeline whose runs are each about
something of their own, such as pull requests.
(pipeline 'site/publish #:steps (collect-steps published) #:web-ui-options '((front-page . latest-run)))
Every step records where in the source its (step …) form
is, and so does the pipeline, and the web interface shows the file and
line beside each step of a run. #:source makes those into
links. It takes a procedure answering the URL where a piece of the
pipeline’s code is published, and (automate) has the
constructors that make one for a forge:
(pipeline 'site/publish #:steps (collect-steps published) #:source (forgejo-source "https://forge.example/owner/site"))
The links are worked out when the pipeline is lowered, not when a page is drawn, so nothing at the pumphouse has to be told where the code lives, and a run from months ago still links to the code that ran. Lowering asks git about each file: a form is linked at the commit the file was at, or on the branch when the file had changes that were not in any commit, since the commit does not hold the code that ran.
forgejo-source serves Forgejo and Gitea. For any other forge,
template-source fills a URL with the position:
(template-source "https://git.example/site.git/tree/{file}?id={commit}#n{line}")
Both link only forms in the pipeline’s own repository, so a step taken
from a library elsewhere is shown without a link rather than linked to
a file that is not there. A procedure of your own may do as it likes
with the two locations it is given, the form’s and the pipeline’s; it
answers a URL string, or #f for no link.
A pipeline without #:source still shows where each step is
defined, without linking. There is one cost to weigh: a location
carries the commit, so any commit touching the file the forms are in
changes the definition’s store path, and the pumphouse numbers a new
definition where it would once have recognised the old one.
A pumphouse holds pipelines, their definitions and every run, keeps
the resources shared between pipelines, and serves the web interface.
It runs no step itself. It is started by a script that ends by
calling run-pumphouse from
(automate pumphouse process):
(use-modules (automate pumphouse process)) (run-pumphouse #:state-directory "/var/lib/guile-automate" #:registration-files (list (registration-file (file "/var/run/guile-automate/any") (scope '(/)) (mode #o640))))
$ guile pumphouse.scm pumphouse listening on /var/run/guile-automate/socket admin: /var/lib/guile-automate/admin registration (/): /var/run/guile-automate/any http on port 8787
Everything reaches a pumphouse through a sturdyref: a string naming one
of its objects, which carries the right to use that object. Whoever
holds a sturdyref holds what it names. A pipeline daemon is given a
registration, which lets it attach the pipelines the registration
covers. The automate command is given the admin, which can do
everything. Each takes --pumphouse, with the sturdyref or with a
file holding it, or reads AUTOMATE_PUMPHOUSE. A file keeps the
sturdyref off the command line, where any user of the machine can read
it.
On the pumphouse’s machine they connect through its socket,
/var/run/guile-automate/socket unless #:socket says
otherwise; they take --unix-socket=FILE for another one. Run
from a checkout through pre-inst-env, the pumphouse and the
programs that reach it use guile-automate.socket in the current
directory instead. The
socket is open to every user, since connecting grants nothing without
a sturdyref. The permissions of the socket’s directory decide who may
connect at all, so give that directory the group of the users who run
pipeline daemons.
The state directory holds the databases, one for the pumphouse and one per pipeline, the keys the pumphouse is known by, and the admin file, readable by the pumphouse’s user alone. The keys keep every sturdyref the same when the pumphouse restarts, so pipeline daemons already running attach again.
A one-shot program given a registration records its definition and run
there too, with one-shot as the run’s cause, without disturbing a
pipeline daemon serving the same pipeline. It uploads its steps’ logs
as a pipeline daemon would, waiting up to thirty seconds after the run
for the upload to finish, and warns when something was not sent.
Interrupted, or sent SIGTERM, it aborts the run with the reason
one-shot program interrupted and reports that before it exits;
a second signal makes it exit at once, leaving the run for the
pumphouse to abandon once the program’s heartbeats have stopped for
#:lost-after seconds.
Everything about a pipeline’s runs is in that pipeline’s database, under pipelines/ in the state directory. So one pipeline’s history can be copied, dropped or trimmed on its own, with the pumphouse stopped, by dealing with the one file. The pumphouse’s own database holds what crosses pipelines: which pipeline each run id belongs to, the links between runs, and the registrations.
Configuration is code. The script can compute what it passes, read
a file, or check the environment, before it calls
run-pumphouse, which runs on the calling thread until the
process is told to stop.
#:pid-file names a file the pumphouse writes its process id to
once it has started: its socket is listening and every file holding a
sturdyref is written. A service manager that waits for a PID file, as
the Shepherd does with the #:pid-file of
make-forkexec-constructor, then knows the pumphouse can be
reached. Pipeline daemons need not wait for it, since they keep trying
to connect.
A registration covers a list of patterns. A pattern is a pipeline name,
which covers that pipeline, or a prefix ending in a slash, such as
automate/, which covers every pipeline under it. A slash alone
covers every pipeline. A registration can also be for one pipeline
daemon’s instance alone, so that a pipeline daemon calling itself
anything else is refused through it, as several pipeline daemons for one
pipeline, below, describes. A pipeline daemon that serves pipelines
outside its registration has those refused, which it prints once per
connection, and serves the rest; a pipeline daemon whose every pipeline
is refused tries again later.
Registrations are declared in the script, or granted from the command
line, but not both. #:registration-files takes a list of
registration-file records, each naming a registration and the
file its sturdyref is written to. A pumphouse given them has those
registrations and no other. At each start it revokes every other
registration, and writes each file again. It also refuses
automate registration grant and automate
registration revoke, since its next start would undo them. A
registration taken out of the script is revoked at the next start, and
one whose patterns change gets a new sturdyref.
Declaring them suits a pumphouse run as a system service. Point each
pipeline daemon on the machine at a file. The file’s mode and its
directory then decide which users may run pipelines, and the
registration decides which pipelines. A registration of /
written where a group can read it lets that group’s users run any
pipeline. A registration per pipeline daemon keeps each one to its own
pipelines.
Without #:registration-files, automate registration grant
PATTERN... mints a registration and prints its sturdyrefs, one for
the socket and, when the pumphouse listens on TCP, one for other
machines. The patterns are the registration’s identity: asking for
the same ones in any order answers the same sturdyref.
automate registration revoke PATTERN... with the same patterns
withdraws it, and automate registration list lists them.
--instance=NAME on grant and revoke says which
instance a registration is for. Registrations survive a restart of
the pumphouse.
The web interface, step log uploads and webhooks are served over
HTTP, at http://[::1]:8787/ unless #:http takes an
http-configuration saying otherwise. Its port is where
they are served; zero means any free port, and the port bound is
written to http-port in the state directory. With
#:http #f there is no web interface and step logs are not
kept.
The server listens on IPv6 loopback by default, so only this machine
reaches it, and over IPv6 alone: ‘localhost’ reaches it from a
browser, which tries both families, but ‘127.0.0.1’ does not. A
host without IPv6 cannot bind ::1, and the pumphouse fails to
start there until (address "127.0.0.1") is given. address
binds it to another IPv4 or IPv6 address, or with 'any to every
address of both families, and public-url names the server as
pipeline daemons on other machines and forges reach it. A pumphouse
behind a proxy that terminates TLS keeps the server on loopback and
names it by the proxy’s URL; one reached directly binds every address.
The server speaks plain HTTP itself, and closes a connection that has
been idle between requests for a minute.
The server can face the internet. An upload of a step’s log is refused before its body is read when it has no content length, when the body is over 256 kibibytes, or when the token it bears is not the one the run was given. So whoever can reach the port cannot make the pumphouse hold bytes. What the web interface shows to everyone is described in The Web Interface.
metrics? adds /metrics to the server, where Prometheus,
a time series database that collects metrics over HTTP, reads them.
(http-configuration (metrics? #t))
The pumphouse counts runs as it records them, so reading the metrics queries nothing. Runs requested, started and finished are counted by pipeline and by outcome, with how long each run waited before starting and how long it then took, and how many are requested and not yet finished. Each pipeline also carries when its last run of each outcome finished, which state each pipeline daemon serving it is in, and when each was last heard from, labelled by the pipeline daemon’s instance. The server’s own requests are counted by route, method and status code, and the process reports its memory, processor time and garbage collection.
The series worth an alert first is
automate_run_last_finish_timestamp_seconds. A pipeline that has
quietly stopped running shows nothing on the pages, since there is no
failed run to see, and an alert on how long ago that pipeline last
succeeded catches it, whether the cause is a paused timer, a pipeline
daemon that never came back or a webhook that stopped arriving.
The server always answers GET /health, for a monitor or a
proxy in front of the pumphouse to ask whether it is working. A
process that has exited is for its service manager to notice; what
the health check finds is a pumphouse still answering HTTP whose
records, or the vat that talks to its pipeline processes, have stopped
answering. Each check sends a message from that vat to the records
and waits for the answer, which touches no database. The check
answers 200 with ‘ok’ when the answer comes back. It answers 503
when there is no answer within five seconds, when the pumphouse is
stopping, or while the previous check is still waiting, and the body
names what it waited on.
Requests are handled one at a time, so a check can wait behind a slow page; give the monitor a timeout of more than five seconds.
The metrics are served to whoever reaches the port, as the pages are. They name pipelines, outcomes and timings, which the pages show already, so a pumphouse whose pages are public can serve them on the same port; one behind a proxy keeps both behind it.
#:tcp takes a (HOST . PORT) pair, and the pumphouse then
also listens for pipeline daemons and the command line over TCP with
TLS, the transport layer security protocol, under a key and certificate
it keeps in the state directory. Its sturdyrefs over TCP name the
pumphouse by its certificate. The admin’s goes to remote-admin
in the state directory.
A pipeline daemon on another machine is given a registration’s sturdyref
over TCP. That is the file of a registration-file with
(transport 'tcp-tls), or the tcp-tls sturdyref that
automate registration grant prints. Copy it to a file on that
machine and give the pipeline daemon the file. The pipeline daemon
dials out, so nothing listens on its host, and it needs no
--unix-socket. A sturdyref carries the port, so a pumphouse
reached from other machines is given a fixed port rather than zero, or
every sturdyref handed out stops working at its next restart.
A timer starts a run of one pipeline at a fixed interval. Timers are
part of the script, as a list of timer-configuration records
under #:timers, so they are there again after every restart
and are read alongside the rest of the configuration:
(run-pumphouse #:state-directory "/var/lib/automate" #:timers (list (timer-configuration (pipeline 'website) (every (seconds-for #:hours 1))) (timer-configuration (name 'website-full) (pipeline 'website) (every (seconds-for #:days 1)) (parameters '((full . #t))))))
every is in seconds, and the first run starts that long after
the pumphouse does. seconds-for turns weeks, days, hours and
minutes into seconds, and (automate pumphouse process) exports
it for a pumphouse script as (automate) does for a definition
file. A tick while no pipeline daemon is attached under the
pipeline’s name starts nothing, so a timer can be configured before
the pipeline it names has ever registered. A timer is known by its
name, which defaults to the pipeline’s, so two timers of one
pipeline need names of their own. Each run a timer starts records
the timer as its cause.
automate timer list lists the timers with their state.
automate timer pause NAME stops one starting runs and
automate timer resume NAME lets it again; the pause is held in
the pumphouse’s memory, so a restart resumes every timer.
Every run starts from a run request carrying an id. A request repeated with the same id, from anything that starts runs, answers the run already started, or already finished, rather than starting another. This is what makes a webhook delivered twice one run (see Forge Webhooks).
A run records what caused it. A timer’s runs have the cause
timer, a forge’s webhook, a one-shot program’s
one-shot, and a run started by trigger-run names the
pipeline and run that started it. A run started with
automate run start has no cause. automate run show
prints the cause, and the pumphouse keeps the links from one run to
another in both directions.
A pipeline daemon sends a heartbeat every ten seconds, or as
--heartbeat says. One unanswered for three intervals, or for
as many seconds as --heartbeat-timeout says, means the pipeline
daemon has lost the pumphouse. On the pumphouse’s side a
pipeline daemon’s attachment to a pipeline is attached when it
registers, live once a heartbeat has arrived, stale after #:stale-after
seconds without one, thirty by default, lost after
#:lost-after seconds without one, ten minutes by default, and
detached once the pipeline daemon has gone.
A pipeline daemon that has lost the pumphouse keeps running its runs and
spools what it has to report, up to ten thousand messages. When the
spool fills, every run the pipeline daemon has under way is aborted with
the reason outbox full. When the pumphouse is back, the pipeline
daemon attaches again and replays the spool. Dialling it is given
limits of its own, since a link that dies in the middle of an attempt
may never answer and never fail either: a pumphouse silent for fifteen
seconds about a pipeline being registered ends the attempt, as does an
attempt that has neither attached nor failed within thirty seconds, or
what --connect-timeout says, and another is made a second later.
A connection accepted but never handshaken, which a pumphouse dying at
that moment leaves, fails the attempt after two seconds, as does an
enlivening unanswered for ten.
Pumphouse limiters, gates and triggers need the pumphouse, so a step
that needs one of those errors while it is away.
The pumphouse keeps the same distance from a pipeline daemon. Asking a
pipeline process for a run, aborting one and unlocking or rejecting a
gate each fail after fifteen seconds of silence, or what the pumphouse
script’s #:answer-timeout says, rather than waiting on a process
that may never answer, so automate run start comes
back, a webhook delivery is answered and a timer’s tick is skipped. A
run asked for that way is recorded before the process is asked, so one
the process starts late is still the same run.
A run request that finds no pipeline daemon serving its pipeline waits
for one to register, as across a pipeline daemon restarting on a new
definition, for as long as the script’s #:dispatch-wait says,
three seconds by default, and is refused after that. A request may
ask for a wait of its own, up to ten times the default: a
webhook-configuration’s wait sets it for the runs its
deliveries start, and trigger-run takes #:wait. Held
requests are kept in the pumphouse’s memory, so a pumphouse restarting
while one waits drops it.
A run does not survive its pipeline daemon. When the pipeline daemon’s
process dies, or is restarted with a new definition, the runs it had
under way are not resumed. They are closed the next time that pipeline
daemon registers the pipeline. A pipeline daemon registers with the
runs it is still reporting on, and the pumphouse finishes every other
unfinished run it took on as abandoned. The run’s running steps
are abandoned, its pending steps skipped, and its last event is
lost, which automate run show prints.
A pipeline daemon that is simply gone is caught by its silence. Once it
is lost, the pumphouse abandons the unfinished runs it took on
the same way. A one-shot program reporting to the pumphouse sends
heartbeats too, every ten seconds or as its --heartbeat says, and
its run is abandoned once it has been silent for #:lost-after
seconds.
The pumphouse also looks over every unfinished run as it checks for
silence, and abandons one that no process has held for
#:lost-after seconds. That catches the runs nothing else would:
those of a pipeline daemon that detached, of one forgotten with
automate pipeline forget-instance, which abandons them at
once, and those whose process was running when the pumphouse restarted
and has not registered since. A one-shot program does not reconnect,
so its run is among those after a restart, even when the program goes
on to finish it. So the records show a run under way for about
#:lost-after seconds at most once its process is gone.
automate run abandon RUN-ID closes a run the same way without
the wait, for a process known to be gone. It stops nothing, and is
refused while the process holding the run is still answering
heartbeats: abort the run instead.
Restart a pipeline daemon when automate run list shows nothing
under way, or accept that those runs are abandoned.
A pipeline that is no longer wanted, renamed or set up by mistake, stays
on the pumphouse’s pages until automate pipeline forget NAME
removes it with every run of it. The pumphouse refuses while a pipeline
daemon is attached under the name, so stop the pipeline daemon, or take
the pipeline out of its definitions, first.
A pipeline can be served by several pipeline daemons at once, on one
machine or on several, to spread its runs across them. Each pipeline
daemon names itself with --instance, and the name has to stay the
same across its restarts and differ from that of every other pipeline
daemon serving the pipeline. A pipeline daemon registering under an
instance already attached replaces that attachment, as a pipeline daemon
restarting does, and the runs of the one it replaced are abandoned. Two
pipeline daemons on one host need an --instance each, since both
default to the host’s name.
A run is whole on one pipeline daemon. The pumphouse hands a new run to
whichever pipeline daemon serving the pipeline holds fewest, and a
pipeline daemon started with --max-runs refuses one past its
bound, whereupon the next is asked. When every one refuses, so is the
run, as busy: the pumphouse keeps no queue, so a timer’s tick is skipped
and a webhook delivery is answered with the error. A request repeated
with a run’s id goes to the pipeline daemon that took the run on, which
is what makes it the same run, and so do aborting it and its gates. A
request to stop a step (see Attaching to a Step) is armed on every
pipeline daemon, and the first to stop the step has the others let the
request go.
Each pipeline daemon’s attachment is its own. One going stale or lost leaves the others as they were, and only the runs it took on are abandoned. The pipeline as a whole shows the state of whichever pipeline daemon serving it is furthest from lost.
The pipeline daemons of one pipeline may have been built from different
code, or from the same code after a different guix pull, and
so register different definitions. One definition serves the pipeline
at a time. New runs go only to the pipeline daemons that registered it,
and the others drain: they finish the runs they have and take nothing
new. The definition serving the pipeline is the one a pipeline daemon
last brought that it did not have before, which is its first
registration, or one with a different definition from its previous. A
pipeline daemon restarting on the definition it had changes nothing, so
an old machine rebooting does not take the pipeline back, while an
upgrade, or a rollback, takes effect with the first pipeline daemon to
register it. automate pipeline serve NAME DEFINITION makes
another definition serve. A pipeline whose serving definition has no
pipeline daemon attached refuses runs rather than run the older code.
For every pipeline daemon to share the work, then, they need the same
definition. Building the file once and copying the result to each
machine with guix copy gives them that; building it on each
machine gives them the same definition only where each has pulled the
same Guix, since a definition that uses the guix-channels
resource captures the Guix it was built with.
A pipeline daemon’s steps use its own machine: its secrets directory,
its Guix daemon, and its store. A step calling a pipeline served
elsewhere is answered with store items in the store of the machine that
ran the called run. A limiter of process scope is one per
pipeline daemon, so it bounds each machine separately, and one of
pumphouse scope is shared by them all, which makes it the way to
bound a pipeline’s runs as a whole (see Resources).
automate pipeline list shows the pipeline daemons serving each
pipeline as its members, each with its instance, state and
definition, and the pipeline’s page lists them. A pipeline daemon gone
for good stays listed as lost until automate pipeline
forget-instance NAME INSTANCE removes it. A registration can be for
one instance, so that the pipeline daemon given it on one machine cannot
replace another’s attachment: an instance in a
registration-file, or automate registration grant
--instance=NAME PATTERN..., makes one, which a pipeline daemon calling
itself anything else is refused through.
automate talks to a pumphouse. A command is a noun and a
verb: the noun names what the command acts on, and automate
NOUN prints the commands of that noun. Every command takes
--pumphouse=STURDYREF, the admin’s sturdyref or a file holding
it, such as admin in the pumphouse’s state directory, and
defaults to AUTOMATE_PUMPHOUSE. Every command also takes
--unix-socket=FILE for a pumphouse whose socket is not the
default one. --help on any command prints its options.
automate pipeline listThe pipelines the pumphouse knows: each one’s attachment, the pipeline
daemons serving it, the definition serving it, how many runs it has and
the ids of the runs still under way. It names no run that has finished,
so its size does not grow with the history; automate run list
NAME is where the rest are.
automate pipeline forget NAMERemove a pipeline and every run of it from the pumphouse, with their output and their links to other runs. Refused while a pipeline daemon is attached under the name.
automate pipeline serve NAME DEFINITIONMake definition number DEFINITION the one serving a pipeline, so that new runs go to the pipeline daemons that registered it and the others drain.
automate pipeline forget-instance NAME INSTANCEStop listing a pipeline daemon that served a pipeline and is gone for good. Refused while it is attached.
automate pipeline drain-and-stop NAME [--instance=NAME] [--reason=TEXT]Have every pipeline daemon attached under a pipeline’s name, or the one
--instance names, take no new run and exit once the runs it
has are done, as a run does through a process-control resource.
Each pipeline daemon’s attachment shows the reason until it has gone.
automate definition list NAMEThe definitions the pumphouse has seen of a pipeline, numbered in the order they were seen, each with its store path.
automate run start NAME [--param=NAME=DATUM]… [--param-string=NAME=STRING]… [--wait]Start a run and print its id. --param reads a parameter’s
value as Scheme and --param-string takes it as a string
(see Parameters). With --wait, follow it to its
outcome, print the status of each step, and exit non-zero when the
run errored.
automate run list NAME [--limit=N] [--param=NAME=DATUM]… [--param-string=NAME=STRING]…The newest runs of a pipeline, summarised: id, definition, parameters,
outcome and timestamps. --limit says how many, twenty by
default. --param and --param-string keep the runs
given those values, read as they are for run start, so
--param=number=12 lists the runs a Forgejo webhook started for
pull request 12.
automate run show RUN-IDA run’s record: its parameters, cause, outcome and the status of each step.
automate run output RUN-ID STEP [--from=N] [--follow]A step’s log, as the pumphouse holds it, from byte N on.
With --follow the command goes on printing what arrives until
the run is over; it asks the pumphouse again every second, since the
admin object has no way to be told.
automate run abort RUN-ID [--reason=TEXT]End a run: its pending steps are skipped and the run is errored.
automate run abandon RUN-ID [--reason=TEXT]Record a run as abandoned, as the pumphouse does once the process holding it is lost, for a process known to be gone sooner. Refused while that process is answering heartbeats.
automate gate list RUN-IDThe gates a run has open, each with its state, the number of parties it
needs and who has approved so far. A gate whose pipeline process does
not answer is listed with an unreachable state rather than
failing the whole command.
automate gate approve RUN-ID GATE [--as=WHO]automate gate reject RUN-ID GATE [--as=WHO] [--reason=TEXT]Advance or refuse a gate. --as names the party, defaulting
to the user’s name, and a gate needing several parties counts
distinct names.
automate repl request NAME STEP [--at=start|end] [--as=WHO]Ask for a step of a pipeline served by a pipeline daemon to be stopped, and drive the session it offers (see Attaching to a Step). It names a pipeline rather than a run because the request is armed in advance, on every pipeline daemon serving the pipeline, and the next run to reach that step takes it.
automate timer listautomate timer pause NAMEautomate timer resume NAMEThe pumphouse’s timers, and stopping and restarting one (see Running a Pumphouse).
automate registration grant PATTERN… [--instance=NAME]automate registration listautomate registration revoke PATTERN… [--instance=NAME]Registrations, for any pipeline daemon or, with --instance, for
the one calling itself NAME. A pumphouse whose script declares
its registrations refuses grant and revoke (see Running a Pumphouse).
The answers are printed as Scheme data, so they can be read back by
a script. automate run output, which prints a step’s log as
it was written, is the one exception.
automate --version says which Automate this is and where its
manual is.
An error is one line on the error port, and the command exits 64 when
the command line itself was not usable and 1 otherwise. Setting
AUTOMATE_BACKTRACE in the environment leaves the exception to
Guile instead, which prints the backtrace a report of a fault in
Automate wants. An error raised inside the pumphouse reaches the
command line without its message: Goblins deliberately carries nothing
of an exception across a connection, so the command can only say that
there was one.
A step can be stopped and held while someone looks at it. Nothing enters the step: the run is asked in advance to stop a step by name, and the step, which already holds a link back to the pipeline process, dials out when it reaches the point named and waits there until it is let go. So there is no moment to catch and nothing to keep alive afterwards. A step nobody asked about runs exactly as it always does.
A one-shot program takes --repl=STEP. The program is the
pipeline process itself, so it stops a step of the run it starts:
$ $(guix build -f hello.scm) --repl=write --param-string=name=World waiting to attach to write
For a pipeline served by a pipeline daemon, automate repl
request asks the pumphouse. The pipeline process dialled out to the pumphouse
and cannot be reached from outside, so the pumphouse relays the request
to it and relays the session back:
$ automate repl request website build --as=ada waiting to attach to build of website
Either command blocks. The request is armed first, and the next run to reach that step takes it; a step already running cannot be told to stop. The request is used once, and a second request for a step already asked about is refused. When the step runs to its end without stopping, or the run ends without the step starting, the command says so and exits.
--as=WHO says who is attaching, as it does for a gate
(see Resources), and defaults to the user’s name.
--at=start, the default, stops the step before its body runs.
--at=end stops it once the body has finished, however it
finished. A one-shot program takes --at beside --repl
in the same way.
--at=end is where a step that failed is looked at. The
session says how the body ended, and hands back the error record with
its backtrace. The workspace, the secrets and everything else the
step holds are as the body left them, since the step has not
finished. Its outputs are settled by then, so a session reads what
the step provided and cannot rewrite it.
When the step stops, the command prints where it stopped, the module the body runs in, the inputs the step was given, and the path of a Unix socket:
attached to write at start module: (automate step-body write) input: work value input: name value nrepl: /run/user/1000/automate-repl-I5tQTF/nrepl.sock connect a client to that socket; write is let go when it disconnects
That socket speaks nREPL, the protocol
Arei and other editors’
Scheme clients connect over. What is evaluated there is evaluated in
the body’s module, with everything the body had: (input 'name)
answers what the step was given, and attach reaches the
resources it holds. Beyond the operations every nREPL client uses, a
session answers automate/inputs and automate/outputs,
saying what the step was given and what it owes. Disconnecting lets
the step go, and the run carries on.
The socket is on the machine the command was typed on, and it is the only socket in any of this. Nothing listens inside the step: what the client sends travels to the step as data, over the link the step already holds.
A session holds everything the step holds, secrets included, so
masking (see Resources) does not survive one. Asking for a step
is an authority over the pipeline, and a pumphouse grants it to
whoever it grants automate run abort and automate gate
approve to. Every operation that runs in a session is recorded as
an event of the run, along with who was attached, so what was done in
one is in the run’s record and not only in the memory of whoever was
there.
A stopped step keeps its place under the process cap and whatever its resources gave it, and other runs wait behind it: the step has not finished with what it holds. Its timeout does not count the time it spends stopped. A run’s page marks a stopped step and says who has it. The web interface stays read-only, and there is no session in a browser.
--repl stops a step of the run the program itself is running,
so a one-shot program given a pumphouse address refuses it.
A pumphouse with an HTTP server serves a read-only web interface at
its root. The front page lists the pipelines, grouped by the part
of the name before its last slash: libs/publish-library is
shown as publish-library under the heading libs/, and
names without a slash come first, under no heading. Each pipeline
is drawn there as its steps alone, left to right, with its
attachment state and active runs. Beneath the drawing are its five
most recent runs, newest first, or, when its web-ui-options
say so, the latest run alone with its statuses on the drawing
(see Pipelines). A run is told by its id, its state and when it
came to be so, with the parameters it was given beneath, a name and a
value to a line, but for one too long to show there, such as a
webhook’s payload, which the run’s page has.
A pipeline’s page, at /pipelines/NAME, draws the steps of the
definition serving it, lists its runs, and lists the pipeline daemons
serving it, saying which drain an older definition. Each parameter
value on the pages links to the runs of the pipeline given that value,
which is how to see every run of one pull request. A value longer than
512 characters, such as a webhook’s payload, is folded away
beneath its length instead, and not linked. The link’s query holds
param=NAME=DATUM, the value written as Scheme, and may repeat to
give several; a string is written with its quotes, so
param=ref="refs/heads/main". The resources and parameters the
steps draw on are in the definition’s structure, which the pages leave
out of the drawing. A step that calls another pipeline is drawn as one:
its node carries a second frame inside the first and says which pipeline
it calls, beneath the step’s own name. The called pipeline is never
drawn inside the caller’s diagram, its structure not being known there
and differing from one run to the next. A pipeline’s page says which
pipelines it calls and which call it.
A run’s page, at /runs/RUN-ID, draws the run over its
definition, each step marked by status, and tables its steps with
when each started and how long it took, drawn besides as a bar on a
timeline along the run, so what ran alongside what is seen at a
glance. Its events, as they were recorded, are folded away beneath
their count. Where any of its steps calls a pipeline, the steps gain
a column naming the runs each started, linked. It says what the run
answered with, where its pipeline declares outputs (see Pipelines).
It also says where the run came from and what came of it. The run
that caused this one is named beside its pipeline and linked. The
runs this one caused are listed under theirs, grouped by pipeline and
counted, since one run may cause many. A run started by a webhook
delivery, a timer or a one-shot program was caused by no run, and its
cause is described instead: the delivery’s event and action as the
forge names them, the timer’s name, or the one-shot program; a run
asked for directly has none recorded. A run that caused none says
nothing at all, which is most of them. A step’s page, at
/runs/RUN-ID/steps/STEP, shows its log. Ticking ‘Show
timestamps’ above it puts beside each line how long after the step
started the line was written, with the date and time in full when the
pointer rests on it; the box is remembered in the browser’s local
storage, and copying the log copies the lines without the times. The
times are taken as the step writes, a piece at a time, so lines that
reach the pipeline process together share one. A log uploaded by a
pipeline daemon from before the times were kept has none, and no box.
Pages update while a run is under way.
?orientation=lr on a pipeline or run page draws the diagram
left to right rather than top to bottom. A status is never shown by
colour alone, for readers who cannot tell red from green. Each has
a line style, solid for what went to plan, dashed for what did not
or has not yet, and dotted for groups, resources and parameters, a
mark on the node’s corner, and its word, in a title on the node for
assistive technology and beside the mark in the tables. A running
step’s line marches, unless the reader’s browser asks for reduced
motion. The pages follow the system’s choice of a light or dark
scheme unless the reader picks one with the button in the header,
which is remembered in the browser’s local storage.
Nothing on the interface starts, aborts, approves or rejects anything. It is meant to face the public, and everything recorded is visible to whoever can reach the port: parameters, output values, error messages, who approved a gate, and everything a step printed. What the pipeline records is the boundary, so a step that prints a secret has published it.
The pages update from server-sent events, a one-way stream of
messages from a server that a browser reads with
EventSource. Anything else that speaks HTTP can read the
same streams:
/events/runs/RUN-IDThe run’s record as a state event, then each event of the run
as it happens, its id being the event’s position within the run,
then finished, after which the stream ends.
/events/output/RUN-ID/STEPThe step’s log held so far, then each chunk as it arrives, the id being
the number of bytes held after it. ?from=N starts at a byte
offset.
/events/pumphouseAn activity event for each run requested, started or
finished, for each event of a run under way, and for each pipeline
whose attachment changes state. An attachment is published when its
state changes and not on every heartbeat.
Payloads are JSON. A client that reconnects sends the last id it
saw as the Last-Event-ID header, and the stream resumes from
there, so nothing is missed across a dropped connection.
A forge starts runs by sending webhook deliveries to the pumphouse.
The HTTP server accepts Forgejo’s and Gitea’s at /webhook when
the script gives it a webhook-configuration: the file holding
the secret the forge signs deliveries with, rules saying which
pipeline each kind of delivery starts, and optionally the path the
forge posts to in place of /webhook. The rules are made by
push-rule and pull-request-rule of
(automate forge forgejo).
(use-modules (automate pumphouse process) (automate forge forgejo)) (run-pumphouse #:state-directory "/var/lib/automate" #:http (http-configuration (public-url "https://automate.example.org") (webhooks (webhook-configuration (secret-file "/etc/automate/webhook-secret") (rules (list (push-rule (repository "cbaines/automate") (branch "trunk") (pipeline 'automate/trunk)) (pull-request-rule (repository "cbaines/automate") (pipeline 'automate/pull-request))))))))
The forge has to reach the server, which listens on loopback alone
unless told otherwise: here a proxy on the same machine forwards
https://automate.example.org to it. On the forge, the
webhook’s target is the server’s URL with /webhook appended, its content type is JSON, and its secret is
what the file holds. A delivery whose signature does not match is
refused. A delivery arriving while no pipeline daemon serves the
pipeline, as one does while a pipeline daemon restarts on a new
definition, waits for one as the pumphouse’s #:dispatch-wait
says, or as the configuration’s wait says, and is answered 503
when none registers in time; Forgejo waits five seconds on a delivery
and does not deliver again by itself, so a wait within that is what
keeps a push from being missed.
A push rule starts its pipeline for pushes to the named branch, or
to every branch when it names none. The run’s parameters
are repository, the clone URL, ref, the full reference
pushed, commit, each a string, and payload, the
delivery the forge sent, as a string of JSON. A pull request rule
starts its pipeline when a pull request is opened, reopened or gains
new commits, with the same four parameters plus number, the
pull request’s number as a number. Its ref is
refs/pull/N/head, which the forge serves for fetching. A
pipeline started this way declares some or all of those parameters
and no others: a run lacking a parameter the pipeline declares is
refused.
The payload holds everything else the forge says about the push or
the pull request, such as who opened a pull request, its address on
the forge and the branch it would merge into, so a pipeline decides
for itself what to read. A step declaring it decodes it with
guile-json and reads it with payload-ref:
(let ((payload (json-string->scm #$payload))) (values (payload-ref payload "pull_request" "user" "login") (payload-ref payload "pull_request" "html_url") (payload-ref payload "pull_request" "base" "ref")))
It is the payload of the delivery that started the run. A later delivery for the same commit, after a label is added or the title is edited say, maps to that run and leaves it as it was.
The payload is a parameter like any other, so whoever can read the web interface can read it, folded away beneath its length on a run’s page. For a private repository, that is its commit messages, the names of the files a push changed, and a pull request’s title and description. Most of its text was written by whoever pushed or opened the pull request, so a step treats it as data: a title passed to a shell as part of a command line runs as a command.
Every delivery from a forge arrives at the same path, whichever
repository it is from, so the repository of a rule is what
keeps the pipelines of several repositories apart. It names the
repository as owner/name, and the rule then applies only to
deliveries from that repository. A rule without one applies to
every repository the forge delivers for. Take a pumphouse serving
two repositories whose push rules both name trunk and no
repository. A push to either starts both pipelines, each with the
other’s clone URL as its repository parameter.
A pull request rule without criteria runs the code of anyone who can open a pull request. That code runs on the pumphouse’s machines, with the resources the pipeline gives its steps, such as the network or a token for the forge. On a forge open to the public, a rule should run only the pull requests someone trusted has looked at.
A rule’s criteria is a procedure deciding this. It is called
with the delivery’s payload, the JSON the forge sent, and the rule
starts its pipeline only when it returns true. The usual choice is
to trust people who can push to the repository, and to have one of
them add a label to any other pull request after reading it:
(pull-request-rule (repository "cbaines/automate") (criteria (lambda (payload) (or (not (from-fork? payload)) (label-present? payload "run-pipeline")))) (pipeline 'automate/pull-request))
from-fork? is true for a pull request from a repository other
than the one it is made to. A pull request that is not from a fork
comes from a branch of the repository itself, which only people who
can push to it can create. On Forgejo only people with write access
can add a label. The API chapter lists the other
helpers, author-among?, targets-branch? and
draft?, and payload-ref, which reads any other field of
the payload.
A rule with criteria is considered again when the labels, title or description of a pull request change, as well as when it is opened, reopened or gains commits. Adding the label therefore starts the pipeline for the commit the pull request is at. The forge only sends label changes to a webhook subscribed to them. In the webhook’s settings, choose custom events and tick the pull request label event along with the pull request events.
A pull request stays trusted while it carries the label. Commits pushed to it after that run as they arrive, and nobody has read them. Remove the label when a pull request needs reading again, and add it back once it has been read. A run for a commit that already ran is not started again, so adding the label back only starts runs for new commits.
Criteria are called in the pumphouse’s HTTP server while the forge waits for an answer. They must return quickly and must not make network requests. When criteria raise an exception, their rule starts nothing and the other rules start their runs as usual. The answer to the delivery names that rule’s pipeline with the exception’s message, which the forge shows in its delivery log.
A push rule takes criteria too, called with the push’s
payload. It can skip commits whose message asks for it, as the
example under push-rule in the API chapter does. It can also
ignore the deletion of a branch, which the forge delivers as a push
whose after commit is all zeros.
A pull request rule with a reviewer starts its pipeline when
that user is asked to review a pull request, and not when the pull
request is opened. Name the account the pipeline reviews as, as
Reviewing pull requests below describes. Someone asks that account
for a review, and the run gives it. This suits a pipeline too slow or too
costly to run on every pull request. It also gives people a way to
run the pipeline again: the forge’s button asking for the review
again.
(pull-request-rule (repository "cbaines/automate") (reviewer "automate-bot") (criteria (lambda (payload) (or (not (from-fork? payload)) (review-requester-can-push? payload)))) (pipeline 'automate/pull-request))
While the review is still requested, new commits and reopening the pull request start the pipeline too. The forge clears the request when the account reviews, so nothing starts after the pipeline’s review until someone asks again. A pipeline that never reviews as that account leaves the request open, and runs on every new commit until someone removes the request. A pipeline that should run on every pull request as well gets a second rule, without a reviewer.
Each request starts a run of its own, even for a commit that has already run. So asking again runs the pipeline again, after a run that failed for a reason outside the code, such as a flaky test. A delivery the forge retries is still the same run, as it keeps the delivery’s id. Replaying a delivery from the webhook’s settings starts another run.
Asking for a review is not a sign of trust. The forge lets the
author of a pull request ask for reviewers, even from a fork, so a
rule whose criteria accept every request runs the code of anyone who
can open a pull request. review-requester-can-push? is true
for a request made by someone with push access. The criteria above
run a pull request from the repository itself on any request, and
one from a fork only when someone with push access asks. New
commits to a pull request from a fork start nothing under them, even
while the review is requested, so someone with push access has to
read them and ask again.
Do not read the permissions in the payload yourself. The forge gives the permissions of the user who sent the delivery for a review request, but for new commits and label changes it gives full access, whoever pushed. Criteria reading them would run every commit pushed to a pull request from a fork.
The forge only sends review requests to a webhook subscribed to them. In the webhook’s settings, choose custom events and tick the event for review requests along with the pull request events. Without it, asking for a review starts nothing, and nothing says why.
Each forge signs with a secret of its own, so a pumphouse taking
deliveries from two gives webhooks a list of configurations,
one per forge. A delivery is accepted when its signature matches
one of the secrets, and starts runs by the rules of that
configuration only. This is what keeps the forges apart: a rule’s
repository names an owner and a repository, which two
forges may both have, but a forge cannot start a pipeline configured
under another forge’s secret.
(webhooks (list (webhook-configuration (secret-file "/etc/automate/codeberg-secret") (path "/hooks/codeberg") (rules (list (push-rule (repository "cbaines/automate") (branch "trunk") (pipeline 'automate/trunk))))) (webhook-configuration (secret-file "/etc/automate/work-forge-secret") (path "/hooks/work") (rules (list (pull-request-rule (repository "team/service") (pipeline 'service/check)))))))
The path field is optional and defaults to /webhook.
Two configurations may share a path, since the secrets tell them
apart; a path per forge makes the request log say which forge a
delivery came from, which matters when one is refused for a bad
signature and the answer says nothing else.
The run’s id is derived from the pipeline, the reference and the commit, so a delivery the forge sends twice maps to the run already started rather than starting another. A review request’s run id is derived from the delivery’s id too, so that asking again runs again, as Running when asked for a review above describes. The server answers a delivery with the runs it started, which the forge shows in its delivery log.
(automate forge forgejo) has what a
step needs to talk to the forge. forgejo-report-status posts
a commit status, which the forge shows on the pull request. A body
names the module in its modules field:
(define reported (step (name 'report) (inputs (list fetched net token repository (step-output checked 'value #:required? #f))) (modules '((automate forge forgejo))) (body #~(forgejo-report-status #$repository (call-with-input-file (attach #$token) get-string-all) #$fetched (if (input-present? #$checked) 'success 'failure) #:context "automate/pull-request"))))
A later status under the same context replaces the earlier one. So
a step with no inputs but the network, the token, repository
and commit can post pending as the run starts, and the
forge shows the run from then on rather than only once it ends.
Other forges get a module of the same shape.
A pull request pipeline can give its result as a review instead of,
or as well as, a commit status. forgejo-review approves the
pull request, requests changes, or comments on it. A commit status
is enough for most pipelines. Choose a review when the forge’s
branch protection should count the pipeline’s approval towards the
approvals a merge needs, or when a failing run should block merging
until a later run approves. Which reviews count, and whether a change
request blocks merging, are settings of the branch protection rule.
Review with a token of an account of its own, a bot. The forge
refuses an approval or a change request from the user who opened the
pull request, and forgejo-review then raises
own-pull-request. With a developer’s token, the pipeline
would fail to review every pull request that developer opens.
The review step below follows the checks step and history of the notification example (see Notifications):
(define bot-token (secret-resource 'forge-bot-token)) (define commit (param 'commit)) (define reviewed (step (name 'review) (inputs (list checks history net bot-token repository number commit)) (error-permitted? #t) (modules '((automate helpers notify) (automate helpers secrets) (automate forge forgejo))) (body #~(let ((transition (checks-change (attach #$history) #$checks #:matching '(repository number))) (passed? (checks-passed? #$checks))) (unless (eq? transition 'superseded) (forgejo-review #$repository (read-secret (attach #$bot-token)) #$number (if passed? 'approve 'request-changes) #:body (format #f "Checks ~a~a" (checks-state #$checks) (if (run-url) (string-append ": " (run-url)) "")) #:commit #$commit)) transition))))
It reviews on every run that is not superseded, not only when the result changes. A review is of one commit, and the forge marks it stale when the pull request gains commits, so each new commit needs a review of its own.
A superseded run must not review. Otherwise a run of an older commit that finishes last approves the pull request over the change request of the newer commit’s run, and a broken change can be merged.
Give #:commit the commit the run checked. Without it, the
forge takes the head of the pull request as it receives the review.
When the pull request has gained a commit since the run started, the
review then approves a commit the run never checked, and the forge
does not mark it stale.
A change request is lifted by an approval from the same account, not by a comment. Only an account’s latest approval or change request counts towards merging, and a comment is neither. So a pipeline that requests changes when the checks fail must approve when they pass. If it comments instead, the change request stays and keeps blocking the merge.
Each review tells everyone watching the pull request, where a commit status tells nobody.
Automate’s own pipelines are under .automate/ in its
repository, where a forge would keep its workflow files. They are
the reference for a pipeline that fetches, builds the project’s Guix
package through the Guix daemon with a Guix another pipeline keeps up
to date, and talks to a forge. automate/trunk
and automate/pull-request, in pipelines.scm, are one set
of steps under two names, one started by a push to trunk and one by a
pull request, so that each reports its commit status under its own
context. automate/guix-channels, in the same file, is the
pipeline whose runs hold the channels they build with.
automate/guix-channelsFinds the latest commits of the default channels with
latest-channels, descending from those its own last successful
run found, and then builds the Guix of those commits. A run succeeds
only when that Guix builds, so the other two pipelines, which read its
newest successful run, never build with a Guix that does not. A
pumphouse timer starts it every few hours, and nothing in the
repository pins the channels: going back to earlier ones means
reading an earlier run (see Run History).
automate/pull-requestFetches the pull request’s reference and builds the Guix of the
channels the newest successful automate/guix-channels run
found. Two checks then run side by side. The package check builds
the repository’s own guile-automate package with that Guix, as
someone adding the repository to their channels at that commit would
get it: the Autotools build, the test suite and the manual, so one
step says whether the commit builds and passes its tests, and its
output is the Automate the commit’s pipelines are lowered on. The
formatting check builds the environment of manifest.scm, which
adds to the package’s development inputs the Emacs that
make format runs, runs it, prints what it changed, restores
the tree, and then fails if anything had changed. It works on the
fetched checkout, so the package builds from a worktree of its own.
A failure of either fails the commit status, which the last step
reports with a link to the run; the build’s log, tests and all, is the
package step’s output.
automate/trunkThe same steps for a push to trunk, and a publishing step that
follows the package build alone. It pushes the manual the package
installed under share/doc and its logo to the pages
branch when they changed, under a pumphouse limiter and only while
trunk is still at the commit the run built, so that a run
overtaken by a newer one leaves the site to it. The manual documents
the commit as it stands, so the push does not wait for the formatting
check.
All three are written on the (automate helpers …)
modules, for running git, finding and building the Guix of a set of
channels, building a channel’s package or a profile through the Guix
daemon, and running a script inside the profile, so a pipeline of the
same shape for another project starts from them.
The pipelines take the repository as a parameter, so they run
against a bare clone on disk, which is how they are tried before they
are pointed at the forge. The steps see none of the machine’s files
that they do not declare, so the pipeline daemon is given the clone with
--expose (see Steps and Inputs). Keep the clone out of
/tmp, since each step has a /tmp of its own. The
secrets directory still needs a forgejo-token file, although
nothing reads it for a clone on disk, so any text will do:
$ git clone --bare https://forge.example.org/owner/project ~/project.git
$ mkdir ~/secrets && echo not-a-real-token > ~/secrets/forgejo-token
$ $(guix build -L . -f .automate/pipelines.scm) \
--pumphouse=REGISTRATION --secrets=$HOME/secrets \
--expose=$HOME/project.git &
$ automate run start automate/trunk \
--param-string=repository=$HOME/project.git \
--param-string=ref=refs/heads/trunk --wait
The reporting step posts a commit status only for a repository whose
URL starts with ‘https://’, so a run against the clone reports
nothing. automate/trunk still pushes the manual, to the
pages branch of the clone.
The forge token is the secret file forgejo-token. Git reads it
through a credential helper, and the reporting step sends it with the
commit status. A registration of automate/ covers both pipelines
and nothing else, so it is the one to give the pipeline daemon.
Automate runs on Linux. Only building a definition file needs Guix as a whole; running the result needs less, as below. A pumphouse runs no step, so its machine needs only Automate itself.
A definition file is built with guix build, so the machine
building it needs Guix and a Guix daemon. The machine running the
result needs the store items the build made, and nothing else from
Guix. That is the same machine unless the items are copied to
another one.
A Guix daemon is needed where the program runs only when a step uses
one. That is a step holding the guix-daemon resource, or a
step that builds packages from a guix-channels resource,
which it does through the Guix daemon.
A pipeline with a guix-channels resource takes its packages
from the Guix that builds the definition file, unless the resource
is given channels of its own (see Resources). That Guix has to
be one guix pull made, since the resource records the
channels it was pulled from. A Guix run from a checkout or a
guix shell has no such record, and building the file fails
with no-current-guix.
A step holding the guix-daemon resource connects to the Guix
daemon as the pipeline process’s user. That user needs access to
the Guix daemon’s socket, which is GUIX_DAEMON_SOCKET or Guix’s
default socket.
A pipeline shepherd becomes the shepherd program found on
the path, which the Automate package propagates; #:shepherd
names one elsewhere. A bridge of the system Shepherd runs as root,
since the Shepherd’s socket is root’s, as a service of that Shepherd
started once the socket exists. A step that lowers a definition
with lower-definition needs the Guix daemon and a Guix whose
channels provide guile-automate, unless #:automate
names a package output or a checkout instead (see Pipelines).
Each step runs in Linux namespaces of its own, which give it a
private file system, process table and network (see Steps and Inputs). The pipeline process runs as an ordinary user, so the
kernel has to let an unprivileged process create a user namespace.
Most kernels do. Some distributions turn this off or restrict it:
Debian’s older kernels with the kernel.unprivileged_userns_clone
setting, Ubuntu from 23.10 with
kernel.apparmor_restrict_unprivileged_userns, and any system
with user.max_user_namespaces set to zero.
Where the kernel refuses, each step dies as it starts, before its body
runs, with the error no-confinement in its log. The step is
abandoned and the run errors. --unconfined, given to a
one-shot program or a pipeline daemon, then runs every step as an
ordinary process. Such a step sees the whole machine the pipeline
process can see, with its file system, network and every secret file in
the secrets directory. Its declared resources are only a description,
not a limit.
A run keeps its workspaces in a directory under TMPDIR, or
under /var/tmp where that is unset, and removes it when the
run ends. Point TMPDIR at a file system with room for what the
steps build. Where /tmp is a tmpfs, it is held in memory.
What steps build through the Guix daemon goes into the store
instead.
A pipeline daemon that is killed, or whose machine loses power, cannot
remove the workspaces and logs of the runs it had, and nothing else
will. Give a pipeline daemon run as a service
--working-directory=DIR. It then keeps its logs and its runs’
workspaces in a directory of its own in DIR, named
automate- and six letters or digits. A pipeline daemon that
stops as it should removes its own directory. At start, a pipeline
daemon removes every such directory in DIR owned by its user,
since an earlier pipeline daemon that was killed left it. Nothing else
in DIR is touched, so it can be a directory other programs use.
Give each pipeline daemon a working directory of its own: a second
pipeline daemon started on the same one removes the first one’s
directory while its runs are still using it.
The following is the list of modules provided by this library.
Declare a pipeline named name whose full set of steps is
steps: (pipeline name #:steps steps #:outputs
outputs #:web-ui-options options #:source source).
The structure is validated, and the pipeline’s steps are held in
dependency order.
outputs says what the pipeline answers with when something outside
it runs the pipeline: an alist from a name of the pipeline’s own to the
step output it stands for, selected as a step’s input is, so that a bare
step means its value output. The names are the pipeline’s
interface, and a step can be renamed without disturbing them.
error is not among the names an output may take. Default
(), so the pipeline answers with nothing.
(pipeline 'guix/build-package #:steps (list plan build report) #:outputs `((derivation . ,(step-output build 'drv)) (log . ,report)))
options is an alist the web interface reads; the meaning of each
key is the interface’s. source says where the pipeline’s code is
published: a procedure called, when the pipeline is lowered, with the
source location of each step and of the pipeline itself, and with the
pipeline’s own location, answering the URL of that code or #f for
no link. forgejo-source and template-source make such
procedures. Default #f, so the web interface shows where each
step is defined without linking there.
Return the branch LOCATION’s repository had checked out, or #f
when its file is in no repository or the repository has a detached head.
Return true when LOCATION’s file differed from the commit when the pipeline was lowered: changed in the working tree, staged, or never committed at all. The commit then does not hold the code that runs, so a link should name the branch instead.
Return the column LOCATION’s form is at, counted from zero, or #f
when the form carried none.
Return the commit LOCATION’s repository was at when the pipeline was
lowered, or #f when its file is in no repository or the
repository has no commit yet. The file itself may differ from that
commit; see source-location-changed?.
Return the working directory of LOCATION’s repository, or #f when
its file is in none. Two locations with the same directory are in the
same repository.
Return the file LOCATION’s form is in, relative to its repository’s working directory when it is in one, and the path it was loaded as when it is not.
Return the line LOCATION’s form is on, counted from one, or #f
when the form carried none.
Return the URL of the origin remote of LOCATION’s repository, or
#f when there is none. A source procedure serving several
repositories can tell them apart by this.
Return true if OBJECT is a source location, as a pipeline’s source procedure is called with.
Declare a step: (step (name n) (inputs l) (body
b) field...). The fields are:
nameA symbol, unique among the steps and parameters of a pipeline, which no resource of it may take either.
inputsA list of steps, resources, parameters and step-output
selections. A step or parameter given plainly stands for its
value output; a resource stands for itself. Default ().
bodyA G-expression evaluated in the step’s own process once every input is
satisfied. It refers to an input by splicing it: #$build for its
value output, and #$build:dist, as Guix writes an output
of a package, for its dist output, the same as
#$(step-output build 'dist). The body checks a non-required
input with input-present?, attaches a resource with
attach, and provides outputs with provide. Its value is
the step’s value output unless the step declares outputs.
callsA call, as call-pipeline makes, saying that the step runs another
pipeline instead of a body of its own. Such a step is no process: the
pipeline process starts the run, waits for it, and takes the outputs the
called pipeline answers with. A step has a body or a call, never both
and never neither, which is checked when the pipeline holding it is
made.
outputsThe list of output names the body provides with provide, instead
of value. A step that calls a pipeline takes each of them from
the output of that name the called pipeline answers with. error
is implicit in every step and cannot be declared, as is run in a
step that calls a pipeline. out cannot be declared either, since
#$step:out in a body would read as a plain splice of the
step.
error-permitted?#t when an error in this step is an outcome the pipeline handles,
through the error output, rather than a failure of the run.
Default #f.
timeoutSeconds after which the step’s process is killed and the step errors, or
#f for no limit. seconds-for writes a longer one
readably, as in (timeout (seconds-for #:hours 2)).
labelA string shown in place of the name in the web interface.
groupA symbol naming the group the step is drawn in.
modulesModule names the body needs beyond the default set of (guix build
utils), (ice-9 match), (ice-9 textual-ports) and
(srfi srfi-1).
A step may take its fields from another with (inherit
step), overriding those it gives. The step records where in the
source its form is, and the web interface shows that, linked when the
pipeline says where its source is published (see Pipelines).
Declare the cache called NAME, a symbol: a directory whose contents
outlive the run and the process that made them, kept for later runs of
the pipelines a pipeline process serves. It lies under the cache
directory the process is given, --cache-directory, which the
process takes to be its own and no other’s; a process given none uses a
temporary one, removed when it stops. Attaching answers a directory no
other step holds until the step ends, and blocks while every one is
held. There are at most MAX such directories, each a copy of the cache,
so a larger MAX lets that many steps use the cache at once at the cost
of the space of each copy. Every pipeline the process serves that
declares a cache of the same NAME shares it. A step sees the cache
where it is on the machine, so there is no #:seen-at.
(define downloads (cache-resource 'downloads))
Declare that a step runs the pipeline named TARGET rather than a body of
its own, and return the call to put in the step’s calls field.
parameters says what the one run of TARGET is given. It is either
the parameters themselves, an alist fixed when the pipeline is built, or
a step whose value is such an alist, worked out while the run is going.
The step then provides the outputs it declares, each taken from the
output of that name TARGET answers with, and run besides, which
is the id of the run it started.
(step (name 'build) (inputs (list plan)) (outputs '(derivation)) (calls (call-pipeline 'guix/build-package #:parameters plan)))
each runs TARGET once for each element of a step’s value, which is
a list of such alists, instead. How many elements there are is settled
while the run is going, which is what this is for: a pipeline’s steps
are fixed when it is built, so a step cannot be made per package that
changed, but a run can. Such a step declares no outputs. It provides
runs, the ids of the runs it started in the order of the list,
and results, one entry per element in that same order:
((run . "a17c93ff21") (outcome . succeeded) (outputs . ((derivation . "/gnu/store/..."))))
width bounds how many of those runs are going at once, the next starting as one ends; without it they all start together. It belongs only to a call made for each of a list.
A step whose runs did not all succeed errors, its results still
carrying what each run did. error-permitted? on the step is then
what decides whether the run it is part of fails (see Outputs and Errors).
A step given to parameters or each must be among the calling step’s inputs, so that the call waits for it. Calling needs a pumphouse: the name of a pipeline means nothing without the registry that holds it.
Return every step reachable from ROOTS through their inputs, each once, dependencies before dependents. Traversal stops at resources and parameters.
Declare the file or directory at SOURCE, which the steps holding it read
and cannot write; NAME labels it in what a run shows. SOURCE is an
absolute path on the machine the run is on, and has to be there when a
step attaches it: the resource makes nothing, it only lets a step in.
With SEEN-AT, an absolute path, that is where a step sees it and what
attach answers; without, a step sees it where it is on the
machine. SEEN-AT has no effect on a run whose steps are not confined,
which has no file system of its own to put it in; neither does the
reading being read-only, a step that is not confined reaching the source
whether it declares it or not.
Prefer an input a step computes, a workspace or a store item wherever
one will do: a pipeline naming a place on the machine runs only on a
machine that has it. Where the place is the operator’s to choose rather
than the pipeline’s, --expose is still the answer.
(define sources (file-input-resource #:source "/srv/sources" #:seen-at "/src"))
Declare the place at DESTINATION on the machine that this run replaces;
NAME labels it in what a run shows. DESTINATION is an absolute path,
and its directory has to be there when a step attaches the resource; the
step is given a staging directory of the run’s own instead, and
attach answers the path in it where the body makes the content.
commit of (automate pipeline step), called from the body,
publishes what was staged and points DESTINATION at it, with one rename
of a symbolic link, so that a reader of DESTINATION sees the content a
run published or the one before it and never a half-written tree. A
step that is confined never holds DESTINATION’s directory at all: the
putting in place is the pipeline process’s, which is the authority
--expose hands over and this does not.
COMMIT-BY says what commit publishes. With rename, the default,
it is the staged content itself, which is free; a step can no longer
name it afterwards, but a descriptor the body kept open still writes to
the published files. With copy, it is a copy, so the body shares
nothing with what was published; the copy is copy-on-write where the
file system does that and a plain copy where it does not.
With SEEN-AT, an absolute path, that is where a step sees the content
and what attach answers; without, a step sees it where it is
staged on the machine. SEEN-AT has no effect on a run whose steps are
not confined, which has no file system of its own to put it in; neither
has the destination being out of reach, such a step reaching it whether
it declares this resource or not.
A pipeline naming a place on the machine runs only on a machine that has it, so reach for this where a run has to leave something on the machine it ran on and a store item or a step’s output will not do.
(define site (file-output-resource #:destination "/srv/www/site" #:seen-at "/out/site"))
Return a source procedure for a pipeline whose repository is at URL, its
page on a Forgejo, or Gitea, forge such as
https://codeberg.org/owner/name. A form in the pipeline’s own
repository is linked to its line, at the commit the file was at, or on
the branch when the file had changes not in any commit. A form in
another repository, or outside any, is not linked.
Declare a gate named NAME: attaching it waits for NEEDED distinct parties to unlock it from outside the run, and raises if it is rejected.
Declare a cache for what Guix keeps of channels: their checkouts, which
commits of them it authenticated and the Guixes built from them. It is
a cache-resource called NAME, at most MAX copies of it, of a type
of its own so that a run shows what it holds. build-guix and
latest-channels of (automate helpers guix) take it as
#:cache, attach it themselves and release it when they are done.
(define guix-cache (guix-channels-cache-resource))
Declare the Guix that steps take packages from: the one built from
CHANNELS, a list of channel records of (guix channels), or, when
CHANNELS is not given, the Guix that lowers the definition file, at the
channels and commits its guix pull made it from. NAME labels
it in what a run shows. Attaching returns the directory of that Guix,
whose bin/guix is its command; the helpers take packages from it
once current-guix of (automate helpers guix) names it. The
structure records the channels, so a run shows which collection its
steps built with.
(define channels (guix-channels-resource))
Declare the Guix daemon’s socket, and with it the whole store. NAME
labels it in what a run shows. A confined step holding it sees the Guix
daemon’s public signing key as well, where the machine keeps it, which
is what guix deploy authorizes a target with.
Declare the host’s network, and give it to the steps that list it. NAME
labels it in what a run shows. A step that does not list it runs in a
network namespace of its own, holding a loopback device and nothing
else: it can still talk to itself over 127.0.0.1, and it can open
a connection to nothing else. Attaching returns nothing of use, since
the declaration is what gives the step the network.
(define net (host-network-resource))
Declare a slot that at most MAX steps hold at once. SCOPE says how far the limit reaches. A process limiter, the default, is one object in the pipeline process serving the pipelines that list it, a pipeline daemon or a one-shot program, and bounds the steps of every run there of every pipeline listing it; its NAME only labels it in what a run shows. A pumphouse limiter bounds across every pipeline declaring one of its NAME, which it must be given, on every machine attached to the pumphouse.
(define publish-slot (limiter-resource #:name 'publishing #:max 1 #:scope 'pumphouse))
Return the source location of a form at LINE, counted from one, and COLUMN, counted from zero, of FILE. When the file is in a repository, DIRECTORY is its working directory, FILE is relative to it, COMMIT and BRANCH are those the repository was at, REMOTE is the URL of its origin and CHANGED? is whether the file differed from the commit; otherwise FILE is as loaded and the rest are #f. Lowering makes these, and a test of a source procedure can.
Return a program that runs PIPELINE once. Parameters, the secrets directory and the workspace root are given on its command line.
Declare a parameter named NAME, a symbol: a value a run is given when it
is started, such as a branch or a repository URL. A step lists the
parameter among its inputs and reads its value by splicing it,
#$repository for the one below. A run has to be given every
parameter a step of its pipeline takes, or it is refused; a parameter
has no default value.
(define repository (param 'repository))
Declare a live reference to the trigger of the pipeline named PIPELINE. NAME labels it in what a run shows.
Declare the power to stop pipeline daemons: the one serving the run when
PIPELINE is not given, or those serving the pipeline named PIPELINE,
through the pumphouse. NAME labels it in what a run shows. A step
attaches it and passes what attach returns to
drain-and-stop of (automate pipeline step). A pipeline
daemon told to stop takes no new run, and exits once the runs it has
have finished. This is how a run that upgraded a pipeline daemon’s
definition, by reconfiguring the system it is a service of, has it start
again on the new one: a service manager that restarts a pipeline daemon
that exited starts whatever it now has installed. A pipeline stops the
pipeline daemons of no other pipeline than those its process-control
resources name.
(define control (process-control-resource)) (define site-control (process-control-resource #:pipeline 'site/deploy))
Declare a resource of TYPE named NAME: one object, set up when the process serving the pipelines listing it starts, that every run of them in that process reaches. ARGS is an alist of build-time arguments interpreted by the resource type, which is also what decides what the resource keeps for a run or a step and what it shares between them.
Declare the power to abort the runs of the pipeline named PIPELINE, or
of the pipeline declaring it when PIPELINE is not given. NAME labels it
in what a run shows. A step attaches it and passes what attach
returns to abort-run of (automate pipeline step). Aborting
goes through the pumphouse, so it needs a run under a pumphouse, and a
pipeline aborts the runs of no other pipeline than those its run-control
resources name. Finding the runs to abort is a run-history resource’s
job.
(define control (run-control-resource)) (define deploy-control (run-control-resource #:pipeline 'site/deploy))
Declare the runs of the pipeline named PIPELINE, or of the pipeline
declaring it when PIPELINE is not given, as a step can list them. NAME
labels it in what a run shows. A step attaches it and passes what
attach returns to list-runs of (automate pipeline
step). The runs are the pumphouse’s records, so listing them needs a
run under a pumphouse, and a pipeline lists the runs of no other
pipeline than those its run-history resources name.
(define history (run-history-resource)) (define trunk-history (run-history-resource #:pipeline 'project/trunk))
Return a program that attaches PIPELINES to a pumphouse and serves runs until stopped. The pumphouse address file is given on its command line. A resource two of PIPELINES list is one resource in that program, set up once for both.
Return true if LOCATION and OTHER are in the same repository. A location outside any repository is in the same one as nothing, and neither is #f, a form whose location was not recorded.
Return the number of seconds in weeks, days, hours,
minutes and seconds added together, each zero unless given.
Use it wherever Automate takes a number of seconds, such as a step’s
timeout or a timer’s every. With exact arguments the
result is exact.
(seconds-for #:hours 6) ; 21600 (seconds-for #:hours 1 #:minutes 30) ; 5400 (seconds-for #:days 1/2) ; 43200
There is no #:months or #:years, since their length in
seconds varies.
Declare a read-only file holding the secret named NAME, once per run.
With SEEN-AT, an absolute path, that is where a step sees it and what
attach answers; without, a step sees it where it is in the
directory the run was given. SEEN-AT has no effect on a run whose steps
are not confined.
(define token (secret-resource 'forge-token #:seen-at "/etc/forge-token"))
Declare a shepherd client a shepherd bridge publishes: an object
reaching some services of a Shepherd with some actions, and nothing
else. CLIENT is the secret holding it, a secret-resource: the
bridge wrote its socket and the client’s sturdyref to the file of that
name in the secrets directory. The pipeline process reads the secret
afresh for each run, dials the bridge once and keeps the link for the
runs after, dialling again only when the bridge has gone or the secret
names another; the step is given a handle that passes its messages on,
and never the secret itself, so the step need not, and should not,
declare the secret among its inputs. NAME labels it in what a run
shows. A step attaches it and passes what attach returns to
register-service, shepherd-status, shepherd-start,
shepherd-stop, shepherd-restart or shepherd-load of
(automate pipeline step); the client refuses a service it does
not reach or an action it does not offer. This is how a pipeline
registers and replaces its own pipeline daemon, and how a run that
reconfigured the system restarts the services the reconfigure changed,
since a reconfigure restarts none itself, without the step holding the
Shepherd’s socket, which is the whole authority of the Shepherd’s user.
Whoever can read the file holds the client, so which pipeline daemons
hold it is decided by whose secrets directory the bridge writes it to.
(define own (shepherd-control-resource #:name 'shepherd #:client (secret-resource 'shepherd))) (define web (shepherd-control-resource #:name 'web #:client (secret-resource 'web)))
Select OUTPUT of SOURCE as an input. A required input whose output is never provided skips the consuming step; a non-required one is merely waited for.
Return a source procedure filling TEMPLATE, a URL with placeholders, for
a form in the pipeline’s own repository: {file},
{line} and {column} are its position,
{commit} the commit the file was at and {branch} the
branch. A form whose template needs a value it has not got, such as the
commit of a file with changes not in any commit, is not linked, nor is a
form in another repository or outside any.
(template-source "https://git.example.org/site.git/tree/{file}?id={commit}#n{line}")
Declare a read-write directory, shared by every step holding it and
materialised once per run. NAME labels it in what a run shows. With
SEEN-AT, an absolute path, that is where a step sees it and what
attach answers; without, a step sees it where it is on the
machine. SEEN-AT has no effect on a run whose steps are not confined,
which has no file system of its own to put it in.
A workspace is scratch. The run makes it in a directory of its own,
under TMPDIR or under /var/tmp, and removes that directory
when the run ends, whatever the outcome; what has to stay on the machine
is a file-output.
(define work (workspace-resource #:seen-at "/src"))
Return a rule starting a pipeline on pull requests:
(pull-request-rule (repository owner/name) (pipeline
pipeline) (criteria procedure) (reviewer login)).
Only pipeline, a symbol, is required. Without repository
the rule applies to pull requests of every repository the forge delivers
for.
Without criteria the rule starts its pipeline when a pull request
is opened or reopened or gains new commits. With it, the rule also
considers a pull request whose labels, title or description change,
calls procedure with the delivery’s payload, and starts its
pipeline only when that returns true. label-present?,
from-fork?, author-among?, targets-branch? and
draft? answer the common questions of a payload, and
payload-ref reads the rest. This runs the pull requests of
people who can push to the repository, and those of anyone else once
someone has added the label run-pipeline:
(pull-request-rule (repository "cbaines/automate") (criteria (lambda (payload) (or (not (from-fork? payload)) (label-present? payload "run-pipeline")))) (pipeline 'automate/pull-request))
The procedure runs in the pumphouse’s HTTP server while the forge waits for an answer, so it must return quickly and must not make network requests.
With reviewer, the rule starts its pipeline when the user
login is asked to review a pull request, and not when one is
opened. While that review is still requested, a pull request that gains
new commits or is reopened starts it too. The forge clears the request
when login reviews, so a pipeline that reviews as login runs
once per request. Each request starts a run of its own, even for a
commit already run, so asking again runs the pipeline again. Criteria
apply as they do to any rule. The author of a pull request may ask for
a review, even from a fork, so review-requester-can-push? is how
criteria tell that someone with push access asked:
(pull-request-rule (repository "cbaines/automate") (reviewer "automate-bot") (criteria (lambda (payload) (or (not (from-fork? payload)) (review-requester-can-push? payload)))) (pipeline 'automate/pull-request))
Return the procedure deciding whether RULE starts its pipeline for a payload, or #f when it starts it for every pull request it applies to.
Return the name of the pipeline RULE starts.
Return the repository RULE applies to as owner/name, or #f when it applies to every repository.
Return the login whose review requests start RULE’s pipeline, or #f when the pull requests themselves start it.
Return true if OBJECT is a pull request rule.
Return a rule starting a pipeline on pushes: (push-rule
(repository owner/name) (branch name) (pipeline
pipeline) (criteria procedure)). Only pipeline, a
symbol, is required. Without repository the rule applies to
pushes to every repository the forge delivers for, and without
branch, or with the branch "*", to every branch.
criteria, when given, is called with the push’s payload, as
payload-ref reads it, and the rule starts its pipeline only when
it returns true. This skips commits whose message asks for it:
(push-rule (repository "cbaines/automate") (branch "trunk") (criteria (lambda (payload) (not (string-contains (or (payload-ref payload "head_commit" "message") "") "[skip ci]")))) (pipeline 'automate/trunk))
Return the branch RULE applies to, "*" for every branch.
Return the procedure deciding whether RULE starts its pipeline for a payload, or #f when it starts it for every push it applies to.
Return the name of the pipeline RULE starts.
Return the repository RULE applies to as owner/name, or #f when it applies to every repository.
Return true if OBJECT is a push rule.
Return true if the pull request of PAYLOAD was opened by one of LOGINS, a list of user names on the forge.
Return true if the pull request of PAYLOAD is a draft: the forge says
so, or its title starts with WIP: or [WIP], Forgejo’s
default prefixes for a work in progress.
Comment TEXT, Markdown, on pull request or issue NUMBER of the repository at URL, as the user TOKEN belongs to, and return the new comment’s id. Everyone watching the pull request is told, as for any comment.
Return the (PIPELINE . REQUEST) pairs to start for a delivery of EVENT, the event header, with PAYLOAD, the parsed JSON body, under RULES, a list of push and pull request rules. DELIVERY is the id the forge gives the delivery, which keys the runs review requests start. A rule with a repository only applies to deliveries from that repository, so one webhook listener serves the pipelines of several. An exception raised by a rule’s criteria is not caught.
Post a commit status for SHA in the repository at URL: STATE is
success, failure, error or pending, shown
under CONTEXT with DESCRIPTION, and linking to TARGET-URL when given,
which is what run-url of the body vocabulary answers. A later
status under the same CONTEXT replaces it, so a run can post
pending when it starts and its outcome when it ends. Return the
response code; raise when the forge refuses.
Return the host, owner and name of the repository at URL, an https clone URL, as three values, or three times #f for anything else.
Review pull request NUMBER of the repository at URL as the user TOKEN
belongs to, and return the review’s id. EVENT is approve,
request-changes or comment. BODY is the review’s text,
Markdown, which the forge requires for a change request, and for a
comment. COMMIT is the commit reviewed: give the commit the run
checked. Without it, the forge takes the head of the pull request when
the review arrives, which may be a commit the run never checked.
The forge does not let a user approve or request changes on a pull
request they opened, so the token belongs to an account of its own. Such
a refusal raises own-pull-request. Everyone watching the pull
request is told of each review. Only a user’s latest approval or change
request counts towards merging, so a new approval replaces the same
user’s earlier change request. A comment replaces neither.
(forgejo-review repository token number (if passed? 'approve 'request-changes) #:body (format #f "Checks ~a: [run](~a)" state (run-url)) #:commit commit)
Return the run id a delivery for COMMIT at REF of PIPELINE maps to. The same delivery repeated maps to the same run. DELIVERY, the id the forge gives a delivery, is part of the run id when given, so that each delivery starts a run of its own, as a review request asking again for a commit already run does.
Return true if SIGNATURE, the hex HMAC-SHA256 a forge sends with a delivery, matches BODY, a bytevector, under SECRET.
Show TEXT, Markdown, in one comment on pull request or issue NUMBER of the repository at URL, editing the comment an earlier call with the same MARKER made, or making it when there is none, and return its id. A run reporting on a pull request calls this every time, so the pull request carries one comment saying where it stands rather than a comment a run. The comment carries MARKER in an HTML comment, which the forge does not show, and only a comment of the user TOKEN belongs to is edited.
Editing a comment tells nobody, where making one tells everyone
watching, so a run that should be heard, having broken or fixed
something, calls forgejo-comment as well.
(forgejo-sticky-comment repository token number (format #f "Checks ~a: [run](~a)" state (run-url)) #:marker "checks")
Return true if the pull request of PAYLOAD comes from a repository other than the one it is made to, as when its author cannot push to the repository. A pull request whose source repository the forge does not name, such as one from a fork since deleted, counts as from a fork.
Return true if the pull request of PAYLOAD carries the label NAME. Only people with write access to the repository can label a pull request on Forgejo, so a label is how one of them says a pull request may run.
Return the value under KEYS, strings, in PAYLOAD, a delivery’s body
parsed by guile-json, or #f when there is none. JSON objects are alists
with string keys and arrays are vectors, so (payload-ref payload
"pull_request" "head" "sha") is the commit a pull request is at. A
criteria procedure uses this for what no helper here reads.
Return true if PAYLOAD is a request for a review, made by a user who can push to the repository. Forgejo lets the author of a pull request ask for reviews, even from a fork, so a request alone says nothing about whether someone trusted has looked at the code. For any other delivery this returns #f. Only some deliveries carry the permissions of the user who sent them: for new commits and label changes the forge reports full access, whoever pushed.
Return true if the pull request of PAYLOAD would merge into BRANCH, a
branch name such as "trunk".
Return RULES, raising an automate error unless they are a list of rules,
as push-rule and pull-request-rule of (automate
forge forgejo) make them.
Return a handler for the Knots web server that accepts forge deliveries from FORGES, a list of (SECRET RULES WAIT) entries, one per forge sending deliveries. A delivery is accepted when its signature matches one of the secrets, mapped to runs by the rules listed with that secret, and TRIGGER is called with a pipeline name and a run request for each run to start, expecting the run id; the request asks to wait WAIT seconds for a pipeline process to take it, or, WAIT being #f, as long as the pumphouse waits by default. The handler answers with the runs started, and names each rule whose criteria raised with the exception’s message, which the forge shows in its delivery log. A trigger refusing because no pipeline process can take the run now, every one busy or none attached, is answered 503, since redelivering later may succeed; any other failure is answered 500. It is called with a request and a port for its body.
Lower the definition FILE, a pipeline definition file whose last
expression is a one-shot or a run-pipeline-daemon, with
the Guix current-guix names, and return the store path of the
program it lowers to. AUTOMATE says which Automate the definition is
lowered with, and so runs on: the name of a package in that Guix, by
default "guile-automate", which is built from the channels the
guix-channels resource captured, as every other package a step
takes; the output directory of a package, as a package spliced into the
body lowers to, for the Automate the pipeline’s own definition named, or
as attaching what build-channel-package returned gives, for the
Automate a checkout’s channel defines; or a directory holding
automate.scm, a checkout, for developing Automate itself. Until
guile-automate is in Guix, a channel has to provide it, such as
this repository, which is one. LOAD-PATH is a list of further
directories to put on the load path, such as the directory of a file
that loads modules beside it. Guix’s messages go to the step’s log.
Raises lowering-failed when the file does not lower, which is
what makes this a check of a definition before anything is done with it,
and unknown-package when the Guix has no package of that name.
The program is rooted by nothing here: whatever starts it, such as a
service manager, is what keeps it.
(define lowered (step (name 'lower) (inputs (list work guix-daemon channels)) (modules '((automate helpers definitions))) (body #~(begin (current-guix (attach #$channels)) (lower-definition (string-append (attach #$work) "/.automate/pipelines.scm") #:load-path (list (attach #$work)))))))
Return the store path of the definition of the pipeline named PIPELINE,
a symbol, in PROGRAM, a pipeline daemon program such as
lower-definition answers: the directory the pumphouse records
that definition by, which a run of it is of. trigger-run takes
it as #:definition, so that a run which installed PROGRAM can
have a run of a pipeline on it. Raises no-such-pipeline when
PROGRAM defines no pipeline of that name, and program-failed when
it does not answer.
(trigger-run (attach #$channels) #:definition (program-definition #$lower-step 'automate/guix-channels))
Default value:
#f
Default value:
#f
Default value:
#f
Run git in DIRECTORY with ARGUMENTS, as git -C DIRECTORY
ARGUMENTS..., raising unless it exits with zero. Its output goes where
the step’s does, and it takes its password from
current-git-credentials when that is set.
Put a checkout of BRANCH of the repository at URL under DIRECTORY and
return its path. When the remote has the branch this is git clone
--depth=1 of it; when it has not, it is an empty repository on that
branch with origin at URL, so that a body pushing to a branch
works whether or not the repository has one yet. The checkout is named
after the branch, or after #:into when that says otherwise.
(let ((pages (git-clone-or-init work url "pages"))) (install-file (string-append source "/doc/index.html") pages) (git pages "add" "."))
Run git in DIRECTORY with ARGUMENTS and return the first line it prints,
without its line ending, or "" when it prints nothing. Its exit
status is not checked, so (git-output directory "status"
"--porcelain") answers "" for a clean tree.
Whether the repository at URL has BRANCH, asked from DIRECTORY with
git ls-remote, so a body can clone it when it is there and start
it when it is not. Only a remote that answers without the branch gives
#f; an unreachable remote or a refused token raises as git
does, so a missing branch is never mistaken for one.
git-clone-or-init is this question and both of its answers
together.
(when (git-remote-branch? work url "pages") (git work "clone" "--depth=1" "--branch=pages" url "pages"))
The commit REF is at in the repository at URL, asked from DIRECTORY with
git ls-remote, or #f when the remote has no such ref. REF
is a whole ref name, such as "refs/heads/trunk". An unreachable
remote or a refused token raises as git does, so a ref that could
not be read is never mistaken for one that is gone.
A step publishing something built from a branch asks for the branch again, inside whatever serialises the publishing, and stands aside when it has moved: of several runs publishing at once, the one whose commit is still the branch’s is the one whose work remains.
(attach #$publishing) (if (equal? (git-remote-tip work url "refs/heads/trunk") (git-output source "rev-parse" "HEAD")) (publish-the-site) 'superseded)
Build OUTPUT of the package called NAME, a string, as the channel whose
checkout is the directory CHECKOUT defines it, through GUIX-DAEMON, the
step’s guix-daemon input, with the Guix of GUIX, the step’s
guix-channels input, and return it as a store item the resource
keeps while the run holds it. The channel’s modules are where the
directory field of its .guix-channel says, as
guix pull finds them, and they see the packages of that Guix,
so this is the package someone gets who adds the channel at CHECKOUT’s
commit to those channels. The build’s log goes to the step’s log, tests
and all. A step after this one takes the item as an input and attaches
it to get the package’s path, which lower-definition takes as its
#:automate. Raises build-failed when the package does not
build, the channel has no package of that name, or its modules do not
load.
(build-channel-package #$guix-daemon #$channels (string-append (attach #$work) "/source") "guile-automate")
Build the Guix of CHANNELS, a list of channel records or of the code
channel->code writes, such as latest-channels returns,
through GUIX-DAEMON, the step’s guix-daemon input, and return it
as a store item the resource keeps while the run holds it. A step after
this one takes the item as an input and sets current-guix to what
attaching it returns, to take packages from that Guix. This is what
guix time-machine does: the channels are fetched at their
commits, authenticated where AUTHENTICATE? and they have an
introduction, and built, which needs the host’s network, unless CACHE, a
guix-channels-cache input, already holds that Guix, in which case
nothing is fetched. The cache also keeps the checkouts and what was
authenticated; without one, every channel is cloned afresh and the Guix
found in the store or built again.
(build-guix #$guix-daemon #$update #:cache #$guix-cache)
Return the path of OUTPUT of the package called NAME, a string such as
"git-minimal", in the Guix current-guix names, building it
through the Guix daemon the first time this process asks for it. The
package is the one that Guix defines, at the channels the
guix-channels resource captured, rather than one from the Guix in
the step’s program, which is a library for talking to the Guix daemon
whose collection nobody chose. The Guix daemon is the one
GUIX_DAEMON_SOCKET names, or Guix’s default.
(current-guix (attach #$channels)) (string-append (build-package "coreutils") "/bin/ls")
Return an inferior of the Guix current-guix names: a
guix repl of it, started the first time this process asks and
kept for the rest of the step. Raises no-guix when
current-guix is not set.
Run the guix command of the Guix current-guix names
with ARGUMENTS, a list of strings, and return two values: its exit
status and what it printed on its output port. What it prints on its
error port, its messages and any build logs, goes to the step’s log.
ENVIRONMENT is a list of NAME=VALUE strings to set beside the
step’s own. Guile’s compilation cache goes in a directory removed
afterwards, since a step has no home to keep one in. Raises
no-guix when current-guix is not set.
(current-guix (attach #$channels)) (let ((status output (invoke-guix '("describe")))) (zero? status))
Return the code of CHANNELS, a list of channel records or of the code
channel->code writes, at their latest commits, with the channels
they depend on, as data a step can provide for other steps and other
pipelines to build a Guix from with build-guix. This is what
guix pull does before building: each channel is fetched, a
commit given in it is kept, a channel with an introduction is
authenticated from it, and where CURRENT, channels or their code as this
returned them before, holds a channel, its new commit has to descend
from the one there, so the channels never go back. It fetches through
GUIX-DAEMON, the step’s guix-daemon input, so the step needs the
host’s network too. CACHE is a guix-channels-cache input, which
keeps the checkouts and what was authenticated for the next time;
without one, every channel is cloned afresh. Raises Guix’s own errors,
such as when a commit fails to authenticate or would go back.
(let ((previous (list-runs (attach #$own-history) #:outcome '(succeeded) #:outputs '(update) #:limit 1))) (latest-channels #$guix-daemon %default-channels #:current (match previous ((run) (listed-run-output run 'update)) (() '())) #:cache #$guix-cache))
Default value:
#f
Default value:
#f
Return the transition, as checks-transition names it, from the
summary of the checks of the newest earlier run of the same thing to
CURRENT, this run’s, and that earlier run as a second value, or
#f for none. HISTORY is an attached run-history resource. The
runs compared are those given the same values of the parameters MATCHING
names, and the values PARAMETERS gives, whose OUTPUT of STEP was
provided; STEP is the step providing the summary.
Runs overlap, so a run requested later than this one may already have
provided its summary. Then this one answers superseded and no
run, and says nothing: the later run compared itself with this one, or
with a run before it, and a transition is told once, by the newer run,
rather than twice or out of order. The question is asked at one moment
and not held, which is enough for that: whichever of two runs asks
second sees the other.
(let ((transition (checks-change (attach #$history) #$checks #:matching '(repository number)))) (when (memq transition '(broken fixed changed-failure)) (announce transition)))
Return true if SUMMARY, as summarise-checks returns it, says
every check passed.
Return the state of SUMMARY, passed or failed. SUMMARY is
what summarise-checks returns, or one of those two symbols alone.
Return what changed from PREVIOUS, the summary of the checks of an
earlier run or #f when there is none, to CURRENT, this run’s:
firstthere was no earlier summary;
brokenthe checks passed and now fail;
fixedthey failed and now pass;
still-passingthey passed and still do;
still-failingthey failed and the same checks still fail;
changed-failurethey failed and still do, but other checks fail now.
Return the names of the checks SUMMARY says failed, empty for a summary that is a symbol alone.
Post PAYLOAD, an alist or vector as guile-json writes it, as JSON to
URL, with HEADERS added to the request, an alist from header name to
value. Return the response code; raise post-refused when it is
not a success. The step needs the host’s network, a host-network
input. This is the shape of the web hooks chat services take:
(post-json (read-secret (attach #$chat-hook)) `((text . ,(format #f "~a is ~a: ~a" ref transition (run-url)))))
Mail BODY under SUBJECT to TO, an address or a list of them, from FROM
when given, with HEADERS added, an alist from header name to value. The
message goes to the program current-sendmail names, which reads
the recipients from it. CONFIGURATION is a file of msmtp’s settings,
such as what attach of a secret input returns, passed as
--file: the server to send through, its account and its
password. msmtp refuses a file others can read, so the secret is kept
with mode 600. Raises mail-failed when the program fails. The
step needs the host’s network, a host-network input.
(send-email #:to "team@example.org" #:subject "trunk is broken" #:body (format #f "See ~a~%" (run-url)) #:configuration (attach #$mail-settings))
Return a summary of CHECKS, an alist from the name of each check to whether it passed: plain data to provide as a step’s output and compare with an earlier run’s. The summary says the checks passed when every check did, and failed otherwise, naming the checks that did not, in order.
(summarise-checks `((build . ,(input-present? #$built)) (tests . ,(input-present? #$checked)))) ⇒ ((state . failed) (failed tests))
Default value:
#f
Default value:
#f
Build the profile FILE describes through GUIX-DAEMON, the step’s
guix-daemon input, with the packages of GUIX, the step’s
guix-channels input, and return it as a store item the resource
keeps while the run holds it: the development inputs of the package FILE
evaluates to, what guix shell -D -f FILE would assemble, or
the manifest it evaluates to, what guix shell -m FILE would.
FILE is evaluated by that Guix, so the packages it names are the ones
its channels define, as they would be for a developer running
guix shell from it. A later step takes the item as an input
and attaches it to get the profile’s path. Inputs may collide, as when
a library is propagated both as Guix builds it and as rebuilt with
another Guile, and that is allowed as guix shell allows it.
It prints the file it evaluates, the derivation it computed and the profile it built, so the step’s output says which profile the steps after it run in. When the build fails, that output names the derivation to build again while looking into it.
Run SCRIPT, shell text, with bash in DIRECTORY with the environment of
PROFILE loaded, stopping at the first failing command and raising unless
the script exits with zero. The bash is the profile’s own when it has
one, so a step whose profile carries a shell needs no
guix-channels input; otherwise it is current-bash, or
bash-minimal from the Guix current-guix names. The
profile’s own etc/profile is sourced, which is why the shell does
not run with -u.
Return true if the spliced input was provided: (input-present?
#$built) for the value output of built, or
(input-present? #$built:dist) for its dist output. An
input declared with #:required? #f may settle without being
provided, and this is how a body finds out before using it. The splice
is asked after rather than fetched, so this does not raise when the
input is absent.
(if (input-present? #$checked:status) "success" "failure")
Abort run ID, one of the pipeline CONTROL, an attached run-control
resource, names, for REASON: its running steps are killed, its pending
ones skipped, and it finishes as errored. Return aborting, or
finished when the run has already finished, so that a run racing
to finish is no error. Raises when ID is this run, which may not abort
itself, or a run of another pipeline than CONTROL’s.
(for-each (lambda (run) (abort-run (attach #$control) (assq-ref run 'id) #:reason "superseded")) (list-runs (attach #$history) #:matching '(number) #:before (run-id) #:finished #f))
Attach RESOURCE, a resource or store item received as an input, to this
step and return what identifies it locally: the path of a workspace, a
secret or a file input, the staging path of a file output, the socket of
a guix-daemon, the directory of a guix-channels Guix, the
directory of a cache no other step holds, the path of a store item.
Attaching a limiter waits for a slot, a cache for a directory, and a
gate for its decision. Attaching twice returns the same thing, and what
a step attached is released when its process exits.
Publish what this step staged in RESOURCE, a file output received as an input, and return the place on the machine it was published to. The pipeline process does the publishing, so a step that is confined never holds that place itself; the body makes the content at the path attach answered, and commit puts it there in one step a reader cannot see the middle of. Raises where nothing was staged, where the file output was committed already, or where the place cannot be published to, so a body that has to know commits before it finishes.
Ask a pipeline daemon to drain and stop, for REASON, through CONTROL, an
attached process-control resource. A resource naming no pipeline asks
the pipeline daemon serving this run. One naming a pipeline asks,
through the pumphouse, every pipeline daemon attached under that name,
or the one calling itself INSTANCE when that is given, and raises when
none is attached, unless REQUIRED? is false, in which case it returns
the empty list: a run that installs a pipeline daemon asks that way,
since on its first run there is no pipeline daemon yet. It raises
whatever REQUIRED? says when this pipeline does not name that one. A
pipeline daemon asked takes no new run from then on, so the pumphouse
gives new runs to another pipeline daemon serving the pipeline or
refuses them as busy. Once every run it has has finished and been
reported, it tells the pumphouse it has gone and exits with status 0. It
waits as long as those runs take: a bound on how long is the pipeline’s
to put, with a step timeout. Return stopping for the pipeline
daemon serving this run, or the list of the instances asked. In a
one-shot program, which leaves after its run anyway, asking for its own
process changes nothing and still returns stopping; asking for
the pipeline daemons serving a pipeline needs a pumphouse and raises
without one.
The run that asked records a drain-and-stop event with REASON,
and each pipeline daemon’s attachment shows it is stopping, with REASON,
until it has gone. A pipeline daemon run as a service with the service
manager set to restart it then comes back on whatever definition the
service now names, which is how a run that reconfigured the system its
pipeline daemon is a service of has that pipeline daemon start again on
the new definition, and how a run that deployed to other machines has
their pipeline daemons do the same.
(define control (process-control-resource)) (define restarted (step (name 'restart) (inputs (list control)) (body #~(drain-and-stop (attach #$control) #:reason "system reconfigured"))))
Run MAKE-VOW, a thunk, inside the step’s vat, the Goblins event loop holding the step’s connection to the pipeline process, and wait for the promise it returns. Only a body that uses Goblins directly needs it.
Return OUTPUT of input SOURCE, the name of a step, resource or parameter
this step declares as an input. For a step, that is the value it
provided, its value output unless OUTPUT names another; for a
parameter, the value the run was given; for a resource, a reference to
pass to attach. Raises if the output was not provided, which
input-present? asks first.
(input 'repository) (input 'build 'version)
Return the runs HISTORY, an attached run-history resource, holds that
match the keywords given, newest first unless ORDER and DIRECTION say
otherwise. Each run is an alist of its id, pipeline,
definition, parameters, outcome, when it was
requested, started and finished, and the
outputs OUTPUTS asks for; listed-run-output reads one of
those.
PARAMETERS keeps the runs given each of its (NAME . VALUE) pairs, a
value matching only a value written the same way: the string "12"
is not the number 12. MATCHING names parameters of this run, and
keeps the runs given the same value of each. BEFORE and AFTER, each a
run id such as (run-id) answers, keep the runs requested before
or after that run. PROVIDED keeps the runs where each of its step
outputs was provided. OUTCOME, a list such as '(succeeded
errored), keeps the finished runs that ended so. FINISHED, when
#t, keeps the runs that have finished, and when #f those
that have not; by default, any, it keeps both. ORDER is
requested, started or finished, and a run that has
not yet started or finished is left out when ordering by that time.
DIRECTION is ascending or descending; LIMIT keeps that
many runs at most.
A step output is a step’s name for its value output, or a
(STEP OUTPUT) list for another. A run’s outputs are
(STEP OUTPUT VALUE) lists. Only an output whose value is plain
data is recorded, so OUTPUTS answers nothing for one whose value is a
store item.
(list-runs (attach #$history) #:matching '(repository number) #:before (run-id) #:provided '(checks) #:outputs '(checks) #:limit 1)
Return the value OUTPUT of STEP recorded in RUN, one of the runs
list-runs answers, or #f when the run recorded none or the
query did not ask for it.
Keep VALUE, a string, out of what the run shows from now on: the marker replaces it in this step’s log, in every event, output value and error record of the run. Every attached secret is masked this way without being asked; mask is for a value the body derived, such as a token it exchanged the secret for, and is called before the value is printed or provided. A value the run cannot take raises with the reason: too-short, under eight bytes, too-long, over four kilobytes, or too-many, past the run’s limit. Masking is a net, not a control: a value printed before it was masked, or printed transformed, goes through, and a value that was printed at all has left the step.
Provide OUTPUT of the running step as VALUE, waiting for the pipeline process to record it, so a step taking the output can start while this body carries on. OUTPUT is one the step declares, and VALUE is plain data or a store item. Raises if the step does not declare OUTPUT, or has provided it already.
Register SERVICE, a symbol, with a pipeline shepherd as the pipeline
daemon running PROGRAM, the absolute name of a pipeline daemon program
such as lower-definition answers, through CONTROL, an attached
shepherd-control resource whose client reaches SERVICE and offers
register. The bridge writes the service’s file with PROGRAM and
ARGUMENTS, a list of strings, or, when ARGUMENTS is not given, those the
service was registered with before, if any, followed by what the
pipeline shepherd’s layout gives every pipeline daemon, composed afresh
at each registration so that an option the layout gains reaches a
pipeline daemon registered before it, and has the Shepherd load it.
Return started when the pipeline daemon was not running and has
been started, loaded when it is running: the definition loaded is
then its replacement, and the run asks the pipeline daemon to drain and
stop so that the Shepherd starts it again on the new program; or
unchanged when the service’s file already ran PROGRAM with those
arguments, the layout’s included, and the pipeline daemon is running, in
which case the file is left alone and the pipeline daemon is not to be
bounced. A program is a store item named by its content, so a
definition lowered from a change that left it as it was is
unchanged. A service the Shepherd has disabled, as it does one
that respawns too fast, is enabled before it is started, so a
registration from a run brings back a pipeline daemon that kept dying.
Raises when the client does not reach SERVICE or offer registering, when
the bridge keeps no service files, as a bridge of the system Shepherd
does not, when no arguments are to be had, or when the Shepherd did not
start the pipeline daemon.
(define program (lower-definition "/path/to/pipelines.scm")) (when (eq? (register-service (attach #$shepherd) 'automate program) 'loaded) (drain-and-stop (attach #$process) #:required? #f #:reason "registered from trunk"))
Release RESOURCE, which this step attached, before the step ends, so that a step waiting for the limiter slot it holds can take it.
Return the id of the run this step is part of, as the pumphouse and
automate run show name it, or #f for a run without one.
Run BODY, a thunk, as this process’s step. The program lowering makes of a step calls this with its body, and a body never does. ARGUMENTS holds the socket path and the sturdyref the pipeline process passed. The process connects back to the pipeline process, receives its inputs, and runs BODY on the main thread while a vat on another thread keeps the connection live. It exits only once the pipeline process has recorded the outcome, or once that process is gone: the kernel kills it with its parent, and it leaves on its own when the process’s introduction server is lost.
Return the address of this run’s page on the pumphouse’s web interface,
or #f when no pumphouse with one serves the run: what a commit
status links back to.
(forgejo-report-status repository token commit 'success #:target-url (run-url))
Have the Shepherd load FILE, the absolute name of a file in the store
holding service definitions, through CONTROL, an attached
shepherd-control resource, and return what the Shepherd answered. A
definition of a running service becomes its replacement, taken up when
it next starts. Raises when the client does not offer load, when
FILE is not in the store, or as shepherd-status does.
Restart SERVICE, a symbol, through CONTROL, an attached shepherd-control
resource, waiting for it to start again, and return what the Shepherd
answered. A service whose definition a reconfigure replaced comes back
on the new one. Raises as shepherd-status does.
(define web (shepherd-control-resource #:name 'web #:client (secret-resource 'web))) (define restarted (step (name 'restart-web) (inputs (list web)) (body #~(let ((control (attach #$web))) (shepherd-restart control 'nginx)))))
Start SERVICE, a symbol, through CONTROL, an attached shepherd-control
resource, and return what the Shepherd answered, its running value.
Raises as shepherd-status does.
Return what the Shepherd says of SERVICE, a symbol, through CONTROL, an
attached shepherd-control resource: an alist with the service’s
provides, requires, status, running value,
and whether it is enabled?, respawn?, one-shot?,
transient? and has a pending-replacement?, among others.
Raises when the client does not reach SERVICE or offer status,
when the Shepherd has no such service, or when the bridge cannot be
reached.
Stop SERVICE, a symbol, through CONTROL, an attached shepherd-control
resource, and return what the Shepherd answered. Raises as
shepherd-status does.
Return the address of the page of the step NAME, a symbol, of this run
on the pumphouse’s web interface, or #f when the run has no page:
where to point someone at the output of a step that failed.
(format #f "The [tests](~a) failed" (step-url 'check))
Ask GUIX-DAEMON, a guix-daemon resource received as an input, to keep the store item at PATH for the steps after this one, and return the item, an object to provide as an output. The resource roots it before answering, so call this while the connection that built it is still open.
Start a run of the pipeline TRIGGER, an attached pipeline-trigger
resource, points at, with PARAMETERS, an alist of names and values.
Return the new run’s id. The new run records this one as its cause.
The request waits for a pipeline process serving the pipeline to take
it, as long as the pumphouse waits by default or, with WAIT, that many
seconds, up to ten times the default: for a pipeline whose daemon is
restarting, say. With DEFINITION, the store path of a definition of the
pipeline, such as program-definition of (automate helpers
definitions) answers, the request waits until a process serving that
definition takes it, and is refused once the wait passes with none
attached. This is how a run that installed a new definition has a run
of it, its own pipeline included: a pipeline may not start itself except
by naming a definition of itself other than the one the run asking is
of. The run asking records that its request waits, and that it was
dropped when it is.
(trigger-run (attach #$trunk) #:parameters `((repository . ,#$repository) (ref . ,#$ref) (commit . ,#$commit)) #:definition (program-definition #$lower-step 'automate/trunk) #:wait 60)
Unregister SERVICE, a symbol, from a pipeline shepherd through CONTROL,
an attached shepherd-control resource whose client reaches SERVICE and
offers unregister: the Shepherd unloads the service, stopping it,
and its file is removed, so it does not come back at the Shepherd’s next
start. Return unregistered.
Connect to the pumphouse whose admin STURDYREF names, through SOCKET when it is over a Unix domain socket, and call PROCEDURE with the vat and the admin object, halting the vat afterwards.
Return SOCKET, a file name, raising unless it fits in the address of a Unix domain socket, which holds 107 bytes of it. Binding or dialling one longer fails off the calling thread, where nothing says why.
Return a fresh vat and its mycapn, reaching the pumphouse STURDYREF is on, through SOCKET when it is over a Unix domain socket.
Return where a pumphouse puts its introduction server’s socket, and
where whatever reaches it looks, unless they are told otherwise:
/var/run/guile-automate/socket, or, run from a checkout through
pre-inst-env, guile-automate.socket in the current
directory, so that trying a pumphouse out needs no directory only a
system service may make. The name is relative, which keeps it within
the length a socket’s name may have wherever the checkout is.
Return the sturdyref STRING names: STRING itself when it is a sturdyref, and otherwise the one the file it names holds.
Return two values from STRING: the sturdyref it names, STRING itself
when it is one and otherwise the one the file it names holds, and the
socket written beside it in that file, or #f when STRING is a
sturdyref or the file holds a bare sturdyref, as a pumphouse writes.
Inside a vat, spawn a netlayer that reaches the pumphouse STURDYREF, a string, is on: through the introduction server listening on SOCKET when it is a sturdyref over a Unix domain socket, and over TCP otherwise. Goblins 0.18 stops accepting incoming connections on a netlayer once any of its introduction servers goes away, so the link to a pumphouse, which may die, gets a netlayer of its own.
Raise when SOCKET is needed and not there.
Return four values: the netlayer, a mycapn for it, a promise fulfilled once the netlayer is installed in that mycapn and ready to be dialled through, and a promise that settles when the introduction server behind it goes away, or #f where there is none to lose. The netlayer is installed rather than handed to spawn-mycapn so that there is a readiness promise at all; whoever dials waits on it, since a mycapn installing a netlayer does not yet know it.
Return the transport STURDYREF, a string, is reached over.
Write STURDYREF, a string, to FILE with MODE: the bare sturdyref, or,
with SOCKET, the path of the introduction server it is reached through,
one form holding both, which read-sturdyref-and-socket reads back
and a shepherd bridge writes for its clients. The file is owned by
OWNER, a user name, when one is given, for a process run as root writing
into another user’s directory. The file’s directory is made, readable
by its owner alone, when it is missing. Whatever FILE held is replaced
at once, so that nothing reading it finds half of it.
Configure the HTTP server of a pumphouse, as run-pumphouse takes
under #:http: (http-configuration field...). The
fields are:
portThe port to listen on, 8787 by default; zero means any free one. The
port bound is written to the state directory’s http-port file.
addressAn IPv4 or IPv6 address to bind to, "::1" by default so that only
this machine reaches the server, over IPv6, or 'any for every
address of both families. A host without IPv6 binds "127.0.0.1"
instead.
public-urlThe URL this server is reached at when it is not the address bound:
pipeline daemons on other machines post step output under it, and a
step’s run-url, which a commit status links to, is built from it.
Default #f.
webhooksA webhook-configuration, a list of them when deliveries come from
several forges each signing with a secret of its own, or #f, the
default, for no webhook listener. A delivery is accepted when its
signature matches the secret of one of them, and starts runs by that
one’s rules only.
metrics?Whether to serve this pumphouse’s metrics at /metrics, for
Prometheus, a time series database that reads them over HTTP, to
collect. Default #f. They are served to whoever asks, as the
pages are: they name pipelines, outcomes and timings, which the pages
show already.
Return the address CONFIGURATION binds to, or 'any for every
address.
Return true if CONFIGURATION serves metrics at /metrics.
Return the port CONFIGURATION listens on, zero meaning any free one.
Return the URL pipeline daemons elsewhere reach CONFIGURATION’s server
at, or #f.
Return the webhook configuration of CONFIGURATION, a list of them, or
#f.
Return true if OBJECT is an HTTP configuration.
Return true if OBJECT is a running pumphouse, as start-pumphouse
returns.
Declare one registration of a pumphouse and the file its sturdyref is
written to, in the list run-pumphouse takes under
#:registration-files: (registration-file (file file)
(scope patterns) field...). A pipeline daemon given the
file as its --pumphouse may attach the pipelines the registration
covers, and no other. The fields are:
fileThe absolute name of the file to write. Its directory is made when it is missing. A pipeline daemon under a pipeline shepherd is started on pumphouse in its own secrets directory, so that is where its registration goes.
scopeA list of patterns: a pipeline name, which covers that pipeline, a
prefix ending in a slash, such as website/, which covers every
pipeline under it, or / alone, which covers every pipeline.
instanceThe instance a pipeline daemon registering through it must call itself,
a string, or #f, the default, for any.
transportWhich of its sturdyrefs is written: unix-domain-socket, the
default, for a pipeline daemon on the pumphouse’s machine, or
tcp-tls for one elsewhere, which needs the pumphouse to listen on
TCP.
modeThe file’s permission bits. Default #o600, readable by the
pumphouse’s user alone.
(run-pumphouse #:state-directory "/var/lib/guile-automate" #:registration-files (list (registration-file (file "/var/run/guile-automate/any") (scope '(/)) (mode #o640)) (registration-file (file "/var/lib/guile-automate/builder") (scope '(guix/)) (instance "builder") (transport 'tcp-tls))))
Return the file registration FILE is written to.
Return the instance registration FILE is for, or #f for any.
Return the permission bits of registration FILE.
Return the patterns registration FILE covers, in canonical form.
Return the transport of the sturdyref registration FILE holds.
Return true if OBJECT is a registration file.
Configure one timer of a pumphouse, in the list run-pumphouse
takes under #:timers: (timer-configuration (pipeline
name) (every seconds) field...). The timer starts a
run of the pipeline every seconds seconds for as long as the
pumphouse runs, skipping a tick while no pipeline daemon is attached
under the name or while the timer is paused. The fields are:
pipelineThe name of the pipeline to start, a symbol.
everyThe seconds between one run and the next, a positive number, which
seconds-for writes readably. The first run starts that long
after the pumphouse does.
parametersAn alist of parameter name to value that every run is given. Default empty.
nameThe symbol automate timer list, automate timer pause and
automate timer resume know the timer by. Defaults to the
pipeline name, so two timers of one pipeline need names of their own.
(run-pumphouse #:state-directory "/var/lib/automate" #:timers (list (timer-configuration (pipeline 'website) (every (seconds-for #:hours 1))) (timer-configuration (name 'website-full) (pipeline 'website) (every (seconds-for #:days 1)) (parameters '((full . #t))))))
Return the seconds between the runs timer CONFIGURATION starts.
Return the name of timer CONFIGURATION, a symbol.
Return the parameters of the runs timer CONFIGURATION starts, an alist.
Return the name of the pipeline timer CONFIGURATION starts.
Return true if OBJECT is a timer configuration.
Configure the forge webhook listener of a pumphouse’s HTTP server:
(webhook-configuration (secret-file file) (rules
rules) field...). file holds the secret the forge
signs its deliveries with. rules is a list of the rules
push-rule and pull-request-rule of (automate forge
forgejo) make, saying which pipelines a delivery starts. The optional
path is where the forge posts deliveries, an absolute path such
as "/hooks/codeberg"; the default is "/webhook".
Configurations sharing a path are told apart by their secrets, so giving
each forge a path of its own is a matter of telling from the request log
which forge sent a delivery. The optional wait is how many
seconds a run a delivery starts waits for a pipeline process to take it,
in place of the pumphouse’s #:dispatch-wait: a push landing while
a pipeline daemon restarts is then taken once it is back rather than
refused, so long as the forge waits as long on the delivery.
Return the path the forge posts deliveries to CONFIGURATION at,
"/webhook" by default.
Return the rules of CONFIGURATION, a list of push and pull request rules.
Return the file holding the secret the forge signs deliveries to CONFIGURATION with.
Return how many seconds a run a delivery to CONFIGURATION starts waits for a pipeline process to take it, or #f for the pumphouse’s default.
Return true if OBJECT is a webhook configuration.
Halt PUMPHOUSE from a thread outside its vats: halt the vats taking messages from outside, then close its databases once the records have handled what was sent them before, and halt the records’ vat. A message the live vat sends in the turn it was in when halted is refused by the closed records.
Write the files holding PUMPHOUSE’s sturdyrefs, after which pipeline processes and the command line can reach it: each of its registration files, then the admin’s, in its state directory, readable by its owner alone.
Return the file in PUMPHOUSE’s state directory that
publish-pumphouse! writes the sturdyref of its admin object to,
over the Unix domain socket.
Return the file in PUMPHOUSE’s state directory naming the port its HTTP server is bound to, written when the server starts.
Return the file in PUMPHOUSE’s state directory that
publish-pumphouse! writes the sturdyref of its admin object to,
over TCP, when it listens on TCP.
Run a pumphouse keeping its records under STATE-DIRECTORY until told to stop, on the calling thread. A pipeline process unheard of for STALE-AFTER seconds is marked stale, and one unheard of for LOST-AFTER seconds, ten minutes by default, is marked lost and every unfinished run it took on abandoned, since nothing else will finish them. So is a one-shot program’s run once the program has been silent that long, and any run no process has held for that long: one whose pipeline daemon was forgotten, or did not register again after the pumphouse restarted. With TCP, a (HOST . PORT) pair, it listens for CapTP over TCP+TLS on that port as well, zero meaning any free one, at every address of both families, naming HOST in its sturdyrefs. With HTTP, an http-configuration, by default one on port 8787 of IPv6 loopback, it serves output uploads, the web interface, any webhooks, a health check at /health and, where the configuration asks for them, the metrics at /metrics, and writes the port bound to DIR/http-port; HTTP being #f, it serves none of them and keeps no step output. TIMERS is a list of timer-configurations, each starting a run of its pipeline at its interval while the pumphouse runs. Fibers runs on the main thread alone, so requests are handled one at a time. Asking a pipeline process for a run, aborting one or unlocking a gate fails after ANSWER-TIMEOUT seconds without an answer, fifteen by default, rather than waiting on a process that may never answer. A run request that finds no process serving its pipeline, as across a pipeline daemon restarting on a new definition, waits DISPATCH-WAIT seconds for one, three by default, before it is refused; a request may ask for a wait of its own, up to ten times that.
Told to stop by SIGTERM or SIGINT, it removes its socket, stops taking messages, has its records handle those already sent, closes its databases and exits; a second signal exits at once.
Pipeline processes and the command line on the machine reach the
pumphouse through its introduction server, whose socket is SOCKET,
/var/run/guile-automate/socket by default, or
guile-automate.socket in the current directory when run from a
checkout through pre-inst-env. The socket is open to every
user, so the permissions of the directory it is in say who can connect.
Connecting grants nothing without a sturdyref.
The sturdyref of the admin object goes to admin in
STATE-DIRECTORY, and to remote-admin over TCP, both readable by
the pumphouse’s user alone. REGISTRATION-FILES, a list of
registration-files, declares the pumphouse’s registrations and the files
their sturdyrefs are written to: every other registration is revoked,
and automate registration grant and automate
registration revoke are refused. Without it, the registrations are
those granted on the command line.
PID-FILE, when given, is written last of all, once the pumphouse can be reached and every file holding a sturdyref is written, so that a service manager waiting on it knows the pumphouse has started.
Return the number of seconds in weeks, days, hours,
minutes and seconds added together, each zero unless given.
Use it wherever Automate takes a number of seconds, such as a step’s
timeout or a timer’s every. With exact arguments the
result is exact.
(seconds-for #:hours 6) ; 21600 (seconds-for #:hours 1 #:minutes 30) ; 5400 (seconds-for #:days 1/2) ; 43200
There is no #:months or #:years, since their length in
seconds varies.
Start a pumphouse keeping its records under STATE-DIRECTORY, creating it and its parents as needed, and return it. A pipeline process unheard of for STALE-AFTER seconds is marked stale, and for LOST-AFTER seconds lost, its unfinished runs abandoned, as are those no process has held for LOST-AFTER seconds. With TCP, a (HOST . PORT) pair, it listens on that port as well, zero meaning any free one, naming HOST in the sturdyrefs it hands out. Its introduction server listens on SOCKET. TIMERS is a list of timer-configurations to run. REGISTRATION-FILES, a list of registration-files or #f, are its registrations, when given, instead of what the admin granted. What it asks of a pipeline or step process fails unanswered after ANSWER-TIMEOUT seconds, fifteen by default. A run request asking for no wait of its own waits DISPATCH-WAIT seconds, three by default, for a pipeline process to take it. Nothing can find it until publish-pumphouse! writes the files that hold its sturdyrefs.
Declare a client of a Shepherd for a shepherd bridge to publish:
(shepherd-client (services names) field...). It is a
secret: the bridge writes its socket and the client’s sturdyref to the
file the alist run-shepherd-bridge takes under #:clients
pairs it with, in the secrets directory of the pipeline daemon that is
to hold the client, and a shepherd-control resource is given that
secret. A step holding the resource can do to the services the client
reaches what its actions offer, and nothing else. The fields are:
servicesThe names of the Shepherd services the client reaches, a non-empty list of symbols.
actionsWhat the client offers, a list drawn from status, start,
stop, restart, load, register and
unregister. The default is status, start,
stop and restart. load has the Shepherd load a
file of service definitions, which is code run as the Shepherd’s user,
so it is offered only for files in the store, which cannot be changed
once built, and only when asked for by name. register and
unregister have the bridge write or remove a pipeline daemon’s
own service file and have the Shepherd load or unload it, which only a
bridge given service files does. A pipeline that keeps its own pipeline
daemon up to date needs status and register.
modeThe file’s permission bits. Default #o600, readable by its owner
alone.
ownerThe user name the file is owned by, or #f, the default, for the
bridge’s own user. A bridge of the system Shepherd runs as root, so it
gives each file to the user whose pipeline daemon holds the client.
(run-shepherd-bridge #:state-directory "/var/lib/guile-automate/shepherd-bridge" #:clients `(("/var/lib/guile-automate/secrets/site-trunk/shepherd-client" . ,(shepherd-client (services '(nginx certbot)) (actions '(status restart)) (owner "automate")))))
Return the actions CLIENT, a shepherd-client, offers on the services it reaches.
Return the permission bits of the file CLIENT, a shepherd-client, writes.
Return the user name the file CLIENT, a shepherd-client, writes is owned
by, or #f for the bridge’s own user.
Return the names of the services CLIENT, a shepherd-client, reaches.
Return true if OBJECT is a shepherd-client.
Run a shepherd bridge until told to stop, on the calling thread: a
process holding the socket of the Shepherd at SHEPHERD-SOCKET, the
system Shepherd’s for root, that lets the steps of pipelines do some of
what herd does. What it offers is CLIENTS, an alist of file
names to shepherd-clients, each an object reaching some services with
some actions; nothing else is offered, and never evaluation of code in
the Shepherd. Each client is written to its file at start, the bridge’s
SOCKET beside its sturdyref: the file is a secret in the secrets
directory of the pipeline daemon that is to hold the client, which a
shepherd-control resource is given, and whoever can read it holds
the client.
The bridge keeps its identity under STATE-DIRECTORY, so the sturdyrefs
it writes are the same across its restarts, and its introduction server
listens on SOCKET, which a shepherd-control resource is given too. A
client offering load loads files in STORE-DIRECTORY alone. With
SERVICE-FILES, of (automate shepherd services), a client offering
register has the bridge write a pipeline daemon’s service file
there, load it and start it, which is what a pipeline shepherd gives its
bridge; a bridge of the system Shepherd is given none, and refuses the
action. With PID-FILE, the process id is written there once every
client’s file is written, for a service manager that waits for one.
Told to stop by SIGTERM or SIGINT, it removes its socket and exits; a second signal exits at once. Run it as a system service under the Shepherd it bridges, as root, started after the Shepherd’s socket exists.
Return the files and directories of a pipeline shepherd kept under
DIRECTORY, as an alist: shepherd-socket, bridge-socket,
services and logs, the Shepherd’s own log under
logs, secrets, work and cache, the roots
each pipeline daemon has a directory of its own under unless the
shepherd is told otherwise, plus the generated configuration and
bridge-script.
Make the directories of a pipeline shepherd under STATE-DIRECTORY and
write its Shepherd configuration and bridge script there, with
SECRETS-DIRECTORY, WORKING-DIRECTORY and CACHE-DIRECTORY, by default
secrets, work and cache under STATE-DIRECTORY, as
the roots each pipeline daemon gets a directory under; ARGUMENTS,
strings every pipeline daemon is started with; and SECRETS, an alist of
file names relative to SECRETS-DIRECTORY to the shepherd-clients the
bridge writes there. Return the alist pipeline-shepherd-files
answers for the directory. This is the whole of what
run-pipeline-shepherd does before becoming the Shepherd.
Become a Shepherd running pipeline daemons, keeping everything under
STATE-DIRECTORY, /var/lib/guile-automate/pipeline-shepherd by
default: its socket, its own log, the service file of each pipeline
daemon registered, their logs, and a shepherd bridge over it, run as one
of its services. A pipeline registers its pipeline daemon through the
bridge with register-service; the Shepherd starts the pipeline
daemon, respawns it when it exits, and loads every pipeline daemon’s
service file again at its next start, so nothing about the pipeline
daemons is declared here.
What is declared is the layout. Every pipeline daemon has a directory
of its own, named after its service, under SECRETS-DIRECTORY,
WORKING-DIRECTORY and CACHE-DIRECTORY, secrets, work and
cache under STATE-DIRECTORY by default, and is started with
--pumphouse= naming the file pumphouse in the first, where
the pumphouse writes its registration, --secrets= naming that
directory, --working-directory= the second and
--cache-directory= the third, where its caches outlive it, after
ARGUMENTS, strings every pipeline daemon is given, and after whatever
the registering pipeline passed to register-service. Whatever
else a pipeline’s steps need as secrets goes in the same directory.
SECRETS is what the shepherd writes into those directories: an alist of
file names relative to SECRETS-DIRECTORY to shepherd-clients of
(automate shepherd bridge), each an object reaching some pipeline
daemons with some actions, written into the directory of the pipeline
daemon that is to hold it, which gives a shepherd-control
resource that secret. A client offering load loads files in
STORE-DIRECTORY alone.
The configuration and the bridge’s script are written afresh at every start. SHEPHERD is the shepherd program to run, the one on the path by default, which this process then becomes, so that what runs under screen or a service manager is the Shepherd itself; stopping it stops the pipeline daemons and the bridge.
(run-pipeline-shepherd #:state-directory "/var/lib/guile-automate/pipeline-shepherd" #:secrets `(("site-trunk/shepherd-client" . ,(shepherd-client (services '(site-trunk site-pull-request)) (actions '(status register))))))
Automate depends on the libraries below. The package propagates all of them, so there is nothing further to install.
Lowers a definition file to store items and builds what the steps build. Step bodies are G-expressions. See GNU Guix Reference Manual.
The actors every process is made of, and the CapTP protocol that connects them. A pumphouse and the pipeline daemons connecting to it over TCP must run the same version, since the wire format changes between releases. See The Spritely Goblins Manual.
The pumphouse’s HTTP server and web interface. Fibers is at https://doc.guix.gnu.org/fibers/latest/en/fibers.html, Knots at https://docs.cbaines.net/guile-knots/ and Safsaf at https://docs.cbaines.net/safsaf/.
The pumphouse’s records, with step logs stored compressed.
Webhook deliveries, commit statuses and the web interface’s event streams. https://github.com/aconchillo/guile-json.
Webhook signatures, run ids, and TLS on the pumphouse’s TCP listener. https://codeberg.org/guile-gcrypt/guile-gcrypt.
The pumphouse’s metrics at /metrics.
https://forge.cbaines.net/cbaines/guile-prometheus.
The nREPL server a step offers on automate repl request.
https://git.sr.ht/~abcdw/guile-ares-rs.
The service manager a pipeline shepherd becomes, to run the pipeline daemons registered with it, and whose protocol a shepherd bridge speaks to the Shepherd it bridges. See The GNU Shepherd Manual.
trigger-run takes #:definition, the store path of a definition
of the pipeline it starts, and #:wait, how many seconds the
request waits for a pipeline process to take it. A request naming
a definition is held by the pumphouse until a process serving that
definition takes it, so a run that installs a new definition can
have a run of it, of its own pipeline included: a pipeline may now
start itself by naming a definition other than the one the run
asking is of. program-definition of (automate helpers
definitions) answers the path for a pipeline of a program
lower-definition lowered, and a pipeline daemon program lists its
definitions given --list-definitions. The run asking records
that its request waits, and that it was dropped when the wait
passed with no such process attached.
run-pumphouse takes
#:dispatch-wait, three seconds by default as before, and
webhook-configuration takes wait, so that a push delivered
while a pipeline daemon restarts on a new definition is taken once
it is back rather than refused. A request’s own wait is capped at
ten times the pumphouse’s default.
Copyright © 2026 Christopher Baines <mail@cbaines.net>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
| Jump to: | A B C D F G H I L M O P R S T U W |
|---|
| Jump to: | A B C D F G H I L M O P R S T U W |
|---|