Bass

guide

This guide glosses over language semantics in favor of being a quick reference for common tasks. If you'd like to learn the language, see bassics.

getting started

Bass is shipped as a single bass binary which needs to be installed somewhere in your $PATH.

To run Bass you'll need either Docker Engine (Linux), Docker Desktop (OS X, Windows), or Buildkit running.

With everything installed, try one of the demos:

bass demos/git-lib.bass

If you see bass: command not found, it's not in your $PATH.

If you see some other kind of error you're welcome to ask for help in GitHub or Discord.

running thunks

𝄢

Bass is built around thunks. Thunks are cacheable commands that produce files and/or a stream of values.

(from (linux/alpine)
  ($ echo "Hello, world!"))
echo "Hello, world!"
{
:image
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659"
}
}
:args
(
  1. "echo"
  2. "Hello, world!"
)
}
𝄢

Throughout this documentation, thunks will be rendered as space invaders to make them easier to identify.

𝄢

To run a thunk's command and raise an error if it fails, call run:

(run (from (linux/alpine)
       ($ echo "Hello, world!")))
stderr: 8 lines
─╮
run {{thunk SOOINRF4LGA54: echo "Hello, world!"}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.12s] echo "Hello, world!"
Hello, world!
null
𝄢

To run a thunk and get true or false instead of erroring, call succeeds?:

(succeeds? (from (linux/alpine)
             ($ sh -c "exit 1")))
stderr: 7 lines
─╮
run {{thunk CI43558RV4D52: sh -c "exit 1"}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.13s] ERROR sh -c "exit 1"
false
𝄢

Thunks are cached forever. They can be cleared with bass --prune, but this should only be necessary for regaining disk space.

𝄢

If you want to run a thunk multiple times, just set a different value as an environment variable. Tip: use now to control cache granularity.

(run (with-env
       (from (linux/alpine)
         ($ echo "Hi again!"))
       {:MINUTE (now 60)}))
stderr: 8 lines
─╮
run {{thunk 0KLNHMGOSJRC4: echo "Hi again!"}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.13s] echo "Hi again!"
Hi again!
null

reading output

𝄢

To parse a stream of JSON values from a thunk's stdout, call read with the :json protocol:

(def cat-thunk
  (from (linux/alpine)
   ; note: stdin is also JSON
    (.cat "hello" "goodbye")))

(let [stream (read cat-thunk :json)]
  [(next stream :end)
   (next stream :end)
   (next stream :end)])
stderr: 9 lines
─╮
read {{thunk 10M3L6SAN5DF2: .cat}}
[0.00s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.12s] .cat
"hello"
"goodbye"
(
  1. "hello"
  2. "goodbye"
  3. end
)
𝄢

To read output line-by-line, set the protocol to :lines:

(-> ($ ls -r /usr/bin)
    (with-image (linux/alpine))
    (read :lines)
    next)
stderr: 149 lines
─╮
read {{thunk PR0R6KDUVRJNA: ls -r /usr/bin}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.13s] ls -r /usr/bin
yes
xzcat
xxd
xargs
whois
whoami
who
which
wget
wc
volname
vlock
vi
uuencode
uudecode
uptime
unzip
unxz
unshare
unlzop
unlzma
unlink
unix2dos
uniq
unexpand
udhcpc6
ttysize
tty
truncate
tree
traceroute6
traceroute
tr
top
timeout
time
test
tee
tail
tac
sum
strings
ssl_client
split
sort
shuf
shred
showkey
sha512sum
sha3sum
sha256sum
sha1sum
setsid
setkeycodes
seq
scanelf
resize
reset
renice
realpath
readlink
pwdx
pstree
pscan
printf
pmap
pkill
pgrep
paste
passwd
openvt
od
nslookup
nsenter
nproc
nohup
nmeter
nl
nc
mkpasswd
mkfifo
microcom
mesg
md5sum
lzopcat
lzma
lzcat
lsusb
lsof
logger
less
ldd
last
killall
ipcs
ipcrm
install
id
iconv
hostid
hexdump
head
hd
groups
getent
getconf
fuser
free
fold
flock
find
fallocate
factor
expr
expand
env
eject
du
dos2unix
dirname
diff
deallocvt
dc
cut
cryptpw
crontab
cpio
comm
cmp
clear
cksum
chvt
cal
bzip2
bzcat
bunzip2
beep
bc
basename
awk
[[
[
"yes"
𝄢

To parse UNIX style tabular output, set the protocol to :unix-table:

(-> ($ ls -r /usr/bin)
    (with-image (linux/alpine))
    (read :unix-table)
    next)
stderr: 149 lines
─╮
read {{thunk PR0R6KDUVRJNA: ls -r /usr/bin}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.00s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.00s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.12s] ls -r /usr/bin
yes
xzcat
xxd
xargs
whois
whoami
who
which
wget
wc
volname
vlock
vi
uuencode
uudecode
uptime
unzip
unxz
unshare
unlzop
unlzma
unlink
unix2dos
uniq
unexpand
udhcpc6
ttysize
tty
truncate
tree
traceroute6
traceroute
tr
top
timeout
time
test
tee
tail
tac
sum
strings
ssl_client
split
sort
shuf
shred
showkey
sha512sum
sha3sum
sha256sum
sha1sum
setsid
setkeycodes
seq
scanelf
resize
reset
renice
realpath
readlink
pwdx
pstree
pscan
printf
pmap
pkill
pgrep
paste
passwd
openvt
od
nslookup
nsenter
nproc
nohup
nmeter
nl
nc
mkpasswd
mkfifo
microcom
mesg
md5sum
lzopcat
lzma
lzcat
lsusb
lsof
logger
less
ldd
last
killall
ipcs
ipcrm
install
id
iconv
hostid
hexdump
head
hd
groups
getent
getconf
fuser
free
fold
flock
find
fallocate
factor
expr
expand
env
eject
du
dos2unix
dirname
diff
deallocvt
dc
cut
cryptpw
crontab
cpio
comm
cmp
clear
cksum
chvt
cal
bzip2
bzcat
bunzip2
beep
bc
basename
awk
[[
[
(
  1. "yes"
)
𝄢

To collect all output into one big string, set the protocol to :raw:

(-> ($ echo "Hello, world!")
    (with-image (linux/alpine))
    (read :raw)
    next)
stderr: 8 lines
─╮
read {{thunk SOOINRF4LGA54: echo "Hello, world!"}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.00s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.00s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.10s] echo "Hello, world!"
Hello, world!
"Hello, world!\n"

providing secrets

𝄢

To shroud a string in secrecy, pass it to mask and give it a name.

(mask "hunter2" :nickserv)
<secret: nickserv (7 bytes)>
𝄢

Secrets can be passed to thunks as regular strings. When serialized, a secret's value is omitted.

($ echo (mask "secret" :password))
echo <secret: password (6 bytes)>
{
:args
(
  1. "echo"
  2. <secret: password (6 bytes)>
)
}
𝄢

Bass does not mask the secret from the command's output. This may come in the future.

𝄢

Sensitive values can end up in all sorts of sneaky places. Bass does its best to prevent that from happening.

  • A thunk's command runs in an isolated environment, so an evil thunk can't* spy on your secrets.
  • A thunk's command (i.e. stdin, env, argv) isn't captured into image layer metadata, so exporting a thunk as an OCI image will not leak secrets passed to it.
  • Secret values are never serialized, so publishing a thunk path will not leak any secrets used to build it.
  • All env vars passed to bass are only provided to the entrypoint script (as *env*). They are also removed from the bass process so that they can't be sneakily accessed at runtime.

With the above precautions, passing secrets to thunks as env vars may often be most ergonomic approach. If you have more ideas, please suggest them!

𝄢

To pass a secret to a command using a secret mount, use with-mount:

(-> ($ cat /secret)
    (with-mount (mask "hello" :shh) /secret)
    (with-image (linux/alpine))
    run)
stderr: 8 lines
─╮
run {{thunk KCSL5Q1GS342G: cat /secret}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.12s] cat /secret
hello
null

* This is all obviously to the best of my ability - I can't promise it's perfect. If you find other ways to make Bass safer, please share them!

caching directories

𝄢

Cache paths may be created using cache-dir and passed to thunks like any other path. Any data written to a cache path persists until cleared by bass --prune.

(def my-cache (cache-dir "my cache"))

(defn counter [tag]
  (from (linux/alpine)
    (-> ($ sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l"
           $tag)
        (with-mount my-cache /var/cache/))))

(defn count [tag]
  (next (read (counter tag) :json)))

[(count "once")
 (count "twice")
 (count "thrice")]
stderr: 19 lines
─╮
read {{thunk G781R2G91SDVI: sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" once}}
[0.01s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
─╮ docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
◀┤ [0.11s] sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" once
1
─╮
read {{thunk CUOK1ATO52RIG: sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" twice}}
◀┤ [0.12s] sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" twice
1
─╮
read {{thunk D0G1T3U645334: sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" thrice}}
◀╯ [0.13s] sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" thrice
1
(
  1. 1
  2. 1
  3. 1
)
𝄢

Currently only one thunk can access a cache path at a time. This may become configurable in the future.

building stuff

passing bits around

𝄢

Thunks run in an initial working directory controlled by Bass. Files created within this directory can be passed to other thunks by using thunk paths.

𝄢

Thunk paths are created by using a thunk with path notation:

(def meowed
  (from (linux/alpine)
    (-> ($ sh -c "cat > ./file")
        (with-stdin ["hello" "goodbye"]))))

meowed/file
sh -c "cat > ./file"
{
:image
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659"
}
}
:args
(
  1. "sh"
  2. "-c"
  3. "cat > ./file"
)
:stdin
(
  1. "hello"
  2. "goodbye"
)
}
./file
𝄢

If the thunk isn't bound to a symbol first, you can use subpath:

(-> ($ sh -c "cat > ./file")
    (with-image (linux/alpine))
    (with-stdin ["hello" "goodbye"])
    (subpath ./file))
sh -c "cat > ./file"
{
:image
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659"
}
}
:args
(
  1. "sh"
  2. "-c"
  3. "cat > ./file"
)
:stdin
(
  1. "hello"
  2. "goodbye"
)
}
./file
𝄢

Just like thunks, a thunk path is just an object. Its underlying thunk won't run until the path is needed by something.

𝄢

When you read a thunk path, Bass runs its thunk and reads the content of the path using the same protocols for reading output:

(next (read meowed/file :json))
stderr: 7 lines
─╮
export path {{thunk RH8AK5OJKKD76: sh -c "cat > ./file"}}/file
[0.00s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.13s] sh -c "cat > ./file"
"hello"
𝄢

When you pass a thunk path to an outer thunk, Bass runs the path's thunk and mounts the path into the outer thunk's working directory under a hashed directory name:

(-> ($ ls -al meowed/file)
    (with-image (linux/alpine))
    run)
stderr: 9 lines
─╮
run {{thunk 2K4BEOLLE98F2: ls -al {{thunk RH8AK5OJKKD76: sh -c "cat > ./file"}}/file}}
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.00s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.13s] sh -c "cat > ./file"
[0.14s] ls -al {{thunk RH8AK5OJKKD76: sh -c "cat > ./file"}}/file
-rw-r--r-- 1 root root 18 Oct 26 1985 ./RH8AK5OJKKD76/file
null
𝄢

If the outer thunk sets a thunk path as its working directory (viw cd or with-dir), you can use ../ to refer back to the original working directory.

(defn go-build [src pkg]
  (-> (from (linux/golang)
        (cd src
          ($ go build -o ./out/ $pkg)))
      (subpath ./out/)))

(def cloned
  (from (linux/alpine/git)
    ($ git clone "https://github.com/vito/bass" ./repo/)))

(go-build cloned/repo/ "./cmd/...")
go build -o ./out/ ./cmd/...
{
:image
{
:image
{
:repository "golang"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:c7e98cc0fd4dfb71ee7465fee6c9a5f079163307e4bf141b336bb9dae00159a5"
}
}
:args
(
  1. "go"
  2. "build"
  3. "-o"
  4. ./out/
  5. "./cmd/..."
)
:mounts
(
  1. {
    :source
    git clone https://github.com/vito/bass ./repo/
    {
    :image
    {
    :image
    {
    :repository "alpine/git"
    :platform
    {
    :architecture "amd64"
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3"
    }
    }
    :args
    (
    1. "git"
    2. "clone"
    3. "https://github.com/vito/bass"
    4. ./repo/
    )
    }
    ./repo/
    :target ./
    }
)
}
./out/
𝄢

Note that any modifications made to an input thunk path will not propagate to subsequent thunks.

𝄢

Astute observers will note that cloned above is not a hermetic, because it doesn't specify a version.

𝄢

The .git module provides basic tools for cloning Git repositories in a hermetic manner.

(use (.git (linux/alpine/git)))

(let [uri "https://github.com/vito/bass"]
  (git:checkout uri (git:ls-remote uri "HEAD")))
stderr: 8 lines
─╮
read {{thunk KDD582G7FDIVQ: git ls-remote https://github.com/vito/bass HEAD}}
[0.01s] resolve image config for docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] docker-image://docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] resolve docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.26s] git ls-remote https://github.com/vito/bass HEAD
d32b936ceed8a32b5ef455ec70263d293ff6f1bf HEAD
git submodule update --init --recursive
{
:image
git checkout d32b936ceed8a32b5ef455ec70263d293ff6f1bf
{
:image
git fetch origin d32b936ceed8a32b5ef455ec70263d293ff6f1bf
{
:image
git clone https://github.com/vito/bass ./
{
:image
{
:image
{
:repository "alpine/git"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3"
}
}
:args
(
  1. "git"
  2. "clone"
  3. "https://github.com/vito/bass"
  4. ./
)
}
:args
(
  1. "git"
  2. "fetch"
  3. "origin"
  4. "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
)
}
:args
(
  1. "git"
  2. "checkout"
  3. "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
)
}
:args
(
  1. "git"
  2. "submodule"
  3. "update"
  4. "--init"
  5. "--recursive"
)
}
./
𝄢

The .git module also provides github, a path root for repositories hosted at GitHub.

git:github/vito/bass/ref/HEAD/
stderr: 8 lines
─╮
read {{thunk 3U6L4QTCNO4O0: git ls-remote https://github.com/vito/bass HEAD}}
[0.00s] resolve image config for docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] docker-image://docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] resolve docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.30s] git ls-remote https://github.com/vito/bass HEAD
d32b936ceed8a32b5ef455ec70263d293ff6f1bf HEAD
git submodule update --init --recursive
{
:image
git checkout d32b936ceed8a32b5ef455ec70263d293ff6f1bf
{
:image
git fetch origin d32b936ceed8a32b5ef455ec70263d293ff6f1bf
{
:image
git clone https://github.com/vito/bass ./
{
:image
{
:image
{
:repository "alpine/git"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3"
}
}
:args
(
  1. "git"
  2. "clone"
  3. "https://github.com/vito/bass"
  4. ./
)
}
:args
(
  1. "git"
  2. "fetch"
  3. "origin"
  4. "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
)
}
:args
(
  1. "git"
  2. "checkout"
  3. "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
)
}
:args
(
  1. "git"
  2. "submodule"
  3. "update"
  4. "--init"
  5. "--recursive"
)
}
./

troubleshooting

When something goes wrong, Bass tries to provide an ergonomic error message. Backtraces show annotated source code complete with syntax highlighting. When a thunk fails its output is included in the error message at the bottom of the screen so you don't have to skim the whole output.

(defn echo-sleep-exit [msg seconds exit-code]
  (subpath
    (from (linux/alpine)
      (with-env
        ($ sleep (str seconds))
        {:AT (now 0)})
      ($ sh -c (str "echo \"$0\"; exit " exit-code) $msg))
    ./))

(defn ls paths
  (run (from (linux/alpine)
         ($ ls & $paths))))

(defn main []
  (ls (echo-sleep-exit "hello\nanother line" 1 1)
      (echo-sleep-exit "oh no" 3 42)))
stderr: 40 lines
─╮
run {{thunk 2TADOMO26JOG0: ls {{thunk MQHB05LBV95GG: sh -c "echo \"$0\"; exit 1" "hello\nanother line"}}/ {{thunk PMERTAR29Q17E: sh -c "echo \"$0\"; exit 42" "oh no"}}/}}
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.00s] resolve image config for docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[1.13s] sleep 1
[1.34s] CANCELED sleep 3
[0.14s] ERROR sh -c "echo \"$0\"; exit 1" "hello\nanother line"
hello
another line
error! call trace (oldest first):
┆ <fs>/multi-fail.bass:15:2..16:37
14 │ (defn main []
15 │ (ls (echo-sleep-exit "hello\nanother line" 1 1)
16 │ (echo-sleep-exit "oh no" 3 42)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
┆ <fs>/multi-fail.bass:11:2..12:26
10 │ (defn ls paths
11 │ (run (from (linux/alpine)
12 │ ($ ls & $paths))))
^^^^^^^^^^^^^^^^^^^^^^^^^
resolve failed: exit code: 1
run summary:
[0.01s] docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[0.01s] resolve docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
○─docker-image://docker.io/library/alpine@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
[1.13s] sleep 1
○─sleep 1
[1.34s] CANCELED sleep 3
──╯ [0.14s] ERROR sh -c "echo \"$0\"; exit 1" "hello\nanother line"
hello
another line
for more information, refer to the full output above

That being said, there's a good chance you'll run into a cryptic error message now and then while I work towards making them friendly. If you find one, please open an issue.

exporting files

𝄢

Thunk paths can be saved in JSON format for archival, auditing, efficient distribution, or just for funsies.

(use (.git (linux/alpine/git)))

(-> ($ go build -o ../out/ "./cmd/...")
    (with-dir git:github/vito/bass/ref/HEAD/)
    (with-image (linux/golang))
    (subpath ./out/)
    (emit *stdout*))
{
  "thunk": {
    "image": {
      "thunk": {
        "image": {
          "ref": {
            "platform": {
              "os": "linux",
              "arch": "amd64"
            },
            "repository": "golang",
            "tag": "latest",
            "digest": "sha256:c7e98cc0fd4dfb71ee7465fee6c9a5f079163307e4bf141b336bb9dae00159a5"
          }
        }
      }
    },
    "args": [
      {
        "string": {
          "value": "go"
        }
      },
      {
        "string": {
          "value": "build"
        }
      },
      {
        "string": {
          "value": "-o"
        }
      },
      {
        "dirPath": {
          "path": "../out"
        }
      },
      {
        "string": {
          "value": "./cmd/..."
        }
      }
    ],
    "dir": {
      "thunk": {
        "thunk": {
          "image": {
            "thunk": {
              "image": {
                "thunk": {
                  "image": {
                    "thunk": {
                      "image": {
                        "thunk": {
                          "image": {
                            "ref": {
                              "platform": {
                                "os": "linux",
                                "arch": "amd64"
                              },
                              "repository": "alpine/git",
                              "tag": "latest",
                              "digest": "sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3"
                            }
                          }
                        }
                      },
                      "args": [
                        {
                          "string": {
                            "value": "git"
                          }
                        },
                        {
                          "string": {
                            "value": "clone"
                          }
                        },
                        {
                          "string": {
                            "value": "https://github.com/vito/bass"
                          }
                        },
                        {
                          "dirPath": {
                            "path": "."
                          }
                        }
                      ]
                    }
                  },
                  "args": [
                    {
                      "string": {
                        "value": "git"
                      }
                    },
                    {
                      "string": {
                        "value": "fetch"
                      }
                    },
                    {
                      "string": {
                        "value": "origin"
                      }
                    },
                    {
                      "string": {
                        "value": "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
                      }
                    }
                  ]
                }
              },
              "args": [
                {
                  "string": {
                    "value": "git"
                  }
                },
                {
                  "string": {
                    "value": "checkout"
                  }
                },
                {
                  "string": {
                    "value": "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
                  }
                }
              ]
            }
          },
          "args": [
            {
              "string": {
                "value": "git"
              }
            },
            {
              "string": {
                "value": "submodule"
              }
            },
            {
              "string": {
                "value": "update"
              }
            },
            {
              "string": {
                "value": "--init"
              }
            },
            {
              "string": {
                "value": "--recursive"
              }
            }
          ]
        },
        "path": {
          "dir": {
            "path": "."
          }
        }
      }
    }
  },
  "path": {
    "dir": {
      "path": "out"
    }
  }
}
null
𝄢

Feeding thunk path JSON to bass --export will print a tar stream containing the file tree.

exporting images

𝄢

Feeding thunk JSON to bass --export will print an OCI image tar stream, which can be piped to docker load for troubleshooting with docker run. This will be made easier in the future.

(emit
  (from (linux/ubuntu)
    ($ apt-get update)
    ($ apt-get -y install git))
  *stdout*)
{
  "image": {
    "thunk": {
      "image": {
        "thunk": {
          "image": {
            "ref": {
              "platform": {
                "os": "linux",
                "arch": "amd64"
              },
              "repository": "ubuntu",
              "tag": "latest",
              "digest": "sha256:d1e2e92c075e5ca139d51a140fff46f84315c0fdce203eab2807c7e495eff4f9"
            }
          }
        }
      },
      "args": [
        {
          "string": {
            "value": "apt-get"
          }
        },
        {
          "string": {
            "value": "update"
          }
        }
      ]
    }
  },
  "args": [
    {
      "string": {
        "value": "apt-get"
      }
    },
    {
      "string": {
        "value": "-y"
      }
    },
    {
      "string": {
        "value": "install"
      }
    },
    {
      "string": {
        "value": "git"
      }
    }
  ]
}
null

special tactics

pinning in bass.lock

𝄢

Bass comes with baby's first dependency pinning solution: memo. It works by storing results of functions loaded from Bass modules into a file typically called bass.lock and committed to your repository.

𝄢

memo takes a bass.lock path, a module thunk, and a symbol, and returns a memoized function.

(def memo-ls-remote
  (memo *dir*/bass.lock (.git (linux/alpine/git)) :ls-remote))
memo-ls-remote
𝄢

Calling the function passes through to the specified function from the loaded module.

(memo-ls-remote "https://github.com/moby/buildkit" "HEAD")
stderr: 8 lines
─╮
read {{thunk 082H6MST9K7SS: git ls-remote https://github.com/moby/buildkit HEAD}}
[0.01s] resolve image config for docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] docker-image://docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] resolve docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.36s] git ls-remote https://github.com/moby/buildkit HEAD
c0a4bd9e75b2b80f9b87705a8647f6a7a9a6a752 HEAD
"c0a4bd9e75b2b80f9b87705a8647f6a7a9a6a752"
𝄢

When the function is called again with the same arguments, the cached response value is returned instead of making the call again:

(memo-ls-remote "https://github.com/moby/buildkit" "HEAD")
"c0a4bd9e75b2b80f9b87705a8647f6a7a9a6a752"
𝄢

Use bass --bump to refresh every dependency in a bass.lock file:

bass --bump bass.lock

The bass --bump command re-loads all embedded module thunks and calls each function with each of its its associated arguments, updating the file in-place.

𝄢

Memoization is mostly leveraged for caching dependency version resolution. For this, your module must define the bass.lock path as a special binding: *memos*.

(def *memos* *dir*/bass.lock)
*memos*
𝄢

The linux and github path roots use this binding to automatically discover the memos location.

(use (.git (linux/alpine/git)))
git:github/vito/bass/ref/main/
stderr: 12 lines
─╮
read {{thunk LJJLB5U936PT4: git ls-remote https://github.com/vito/bass main}}
─╮
resolve {{thunk 27OTTG8R8CRVM: }}
[0.08s] resolve image config for docker.io/alpine/git:latest
[0.01s] resolve image config for docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] docker-image://docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.01s] resolve docker.io/alpine/git@sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3
[0.27s] git ls-remote https://github.com/vito/bass main
d32b936ceed8a32b5ef455ec70263d293ff6f1bf refs/heads/main
git submodule update --init --recursive
{
:image
git checkout d32b936ceed8a32b5ef455ec70263d293ff6f1bf
{
:image
git fetch origin d32b936ceed8a32b5ef455ec70263d293ff6f1bf
{
:image
git clone https://github.com/vito/bass ./
{
:image
{
:image
{
:repository "alpine/git"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:d453f54c83320412aa89c391b076930bd8569bc1012285e8c68ce2d4435826a3"
}
}
:args
(
  1. "git"
  2. "clone"
  3. "https://github.com/vito/bass"
  4. ./
)
}
:args
(
  1. "git"
  2. "fetch"
  3. "origin"
  4. "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
)
}
:args
(
  1. "git"
  2. "checkout"
  3. "d32b936ceed8a32b5ef455ec70263d293ff6f1bf"
)
}
:args
(
  1. "git"
  2. "submodule"
  3. "update"
  4. "--init"
  5. "--recursive"
)
}
./
𝄢

Third-party modules may respect this binding too. Here's how linux is defined, for reference:

(defop linux args scope
  (let [path-root (path {:os "linux"} (:*memos* scope null))]
    (eval [path-root & args] scope)))
linux

sharing bass code

Using bass.lock files lets you share and reuse Bass code in git repos:

; image and git path resolution will be cached here.
;
; images store digests, git paths store shas.
(def *memos* *dir*/git-lib.lock)

(use (.git (linux/alpine/git))
     (git:github/vito/tabs/ref/main/wget.bass))

(defn main []
  (emit (wget:wget "https://example.com" ./index.html) *stdout*))
stderr: 94 lines
─╮
export path {{thunk M8SI6J1M2237G: git submodule update --init --recursive}}/wget.bass
[0.01s] resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2
[0.01s] docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2
[0.00s] resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2
[0.08s] sha256:b4f093b99828d544282f6ae73a3790bf797054598ba594ca61fc01461ed8398c
[0.08s] sha256:ca7dd9ec2225f2385955c43b2379305acd51543c28cf1d4e94522b3d94cce3ce
[0.03s] extracting sha256:ca7dd9ec2225f2385955c43b2379305acd51543c28cf1d4e94522b3d94cce3ce
[0.28s] sha256:eb878e0a08e4e9249bc186e41811e9a281e6016d9ddb39af1df449cfbec600fc
[0.15s] extracting sha256:eb878e0a08e4e9249bc186e41811e9a281e6016d9ddb39af1df449cfbec600fc
[0.00s] extracting sha256:b4f093b99828d544282f6ae73a3790bf797054598ba594ca61fc01461ed8398c
─╮ docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2
◀┤ [0.59s] git clone https://github.com/vito/tabs ./
Cloning into '.'...
[0.38s] git fetch origin 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22
From https://github.com/vito/tabs
* branch 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 -> FETCH_HEAD
[0.20s] git checkout 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22
Note: switching to '6ca3f15d70b739a7929886e20b61dbc4cbdc4e22'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 6ca3f15 nix/linux: build FHS layout
[0.21s] git submodule update --init --recursive
─╮
export path {{thunk M8SI6J1M2237G: git submodule update --init --recursive}}/nix.bass
◀┤ [0.45s] git clone https://github.com/vito/tabs ./
Cloning into '.'...
[0.40s] git fetch origin 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22
From https://github.com/vito/tabs
* branch 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 -> FETCH_HEAD
[0.21s] git checkout 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22
Note: switching to '6ca3f15d70b739a7929886e20b61dbc4cbdc4e22'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 6ca3f15 nix/linux: build FHS layout
[0.24s] git submodule update --init --recursive
─╮
export path {{thunk M8SI6J1M2237G: git submodule update --init --recursive}}/nix.lock
◀╯ [0.87s] git clone https://github.com/vito/tabs ./
Cloning into '.'...
[0.34s] git fetch origin 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22
From https://github.com/vito/tabs
* branch 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 -> FETCH_HEAD
[0.15s] git checkout 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22
Note: switching to '6ca3f15d70b739a7929886e20b61dbc4cbdc4e22'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 6ca3f15 nix/linux: build FHS layout
[0.16s] git submodule update --init --recursive
{
  "thunk": {
    "image": {
      "thunk": {
        "image": {
          "archive": {
            "platform": {
              "os": "linux",
              "arch": "amd64"
            },
            "file": {
              "thunk": {
                "thunk": {
                  "image": {
                    "thunk": {
                      "image": {
                        "thunk": {
                          "image": {
                            "thunk": {
                              "image": {
                                "thunk": {
                                  "image": {
                                    "ref": {
                                      "platform": {
                                        "os": "linux",
                                        "arch": "amd64"
                                      },
                                      "repository": "nixos/nix",
                                      "tag": "latest",
                                      "digest": "sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf"
                                    }
                                  },
                                  "args": [
                                    {
                                      "string": {
                                        "value": "cp"
                                      }
                                    },
                                    {
                                      "string": {
                                        "value": "-anT"
                                      }
                                    },
                                    {
                                      "dirPath": {
                                        "path": "/nix"
                                      }
                                    },
                                    {
                                      "dirPath": {
                                        "path": "/cache"
                                      }
                                    }
                                  ],
                                  "mounts": [
                                    {
                                      "source": {
                                        "cache": {
                                          "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                                          "path": {
                                            "dir": {
                                              "path": "."
                                            }
                                          }
                                        }
                                      },
                                      "target": {
                                        "dir": {
                                          "path": "/cache"
                                        }
                                      }
                                    }
                                  ]
                                }
                              },
                              "args": [
                                {
                                  "string": {
                                    "value": "sh"
                                  }
                                },
                                {
                                  "string": {
                                    "value": "-c"
                                  }
                                },
                                {
                                  "string": {
                                    "value": "echo accept-flake-config = true >> /etc/nix/nix.conf"
                                  }
                                }
                              ],
                              "mounts": [
                                {
                                  "source": {
                                    "cache": {
                                      "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                                      "path": {
                                        "dir": {
                                          "path": "."
                                        }
                                      }
                                    }
                                  },
                                  "target": {
                                    "dir": {
                                      "path": "/nix"
                                    }
                                  }
                                }
                              ]
                            }
                          },
                          "args": [
                            {
                              "string": {
                                "value": "sh"
                              }
                            },
                            {
                              "string": {
                                "value": "-c"
                              }
                            },
                            {
                              "string": {
                                "value": "echo experimental-features = nix-command flakes >> /etc/nix/nix.conf"
                              }
                            }
                          ],
                          "mounts": [
                            {
                              "source": {
                                "cache": {
                                  "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                                  "path": {
                                    "dir": {
                                      "path": "."
                                    }
                                  }
                                }
                              },
                              "target": {
                                "dir": {
                                  "path": "/nix"
                                }
                              }
                            }
                          ]
                        }
                      },
                      "args": [
                        {
                          "string": {
                            "value": "nix"
                          }
                        },
                        {
                          "string": {
                            "value": "build"
                          }
                        },
                        {
                          "string": {
                            "value": "-f"
                          }
                        },
                        {
                          "filePath": {
                            "path": "image.nix"
                          }
                        }
                      ],
                      "mounts": [
                        {
                          "source": {
                            "logical": {
                              "file": {
                                "name": "image.nix",
                                "content": "bGV0CiAgZmxha2UgPSBidWlsdGlucy5nZXRGbGFrZSAiL2ZsYWtlIjsKICBpbnB1dHMgPSBmbGFrZS5pbnB1dHM7CiAgcGtncyA9IGltcG9ydCBpbnB1dHMubml4cGtncyB7fTsKICBmaHMgPSAocGtncy5jYWxsUGFja2FnZSA8bml4cGtncy9wa2dzL2J1aWxkLXN1cHBvcnQvYnVpbGQtZmhzLXVzZXJlbnYvZW52Lm5peD4ge30pIHsKICAgIG5hbWUgPSAiY29udGFpbmVyLWZocyI7CiAgICB0YXJnZXRQa2dzID0gcGtnczogd2l0aCBwa2dzOyBbd2dldF07CiAgICBtdWx0aVBrZ3MgPSBudWxsOyAgIyBEb24ndCBpbmNsdWRlIGdsaWJjJ3MgbXVsdGlsaWIKICB9OwogIHN0cmVhbSA9IHBrZ3MuZG9ja2VyVG9vbHMuc3RyZWFtTGF5ZXJlZEltYWdlIHsKICAgIG5hbWUgPSAibml4cGtncy93Z2V0IjsKICAgIGNvbnRlbnRzID0gcGtncy5zeW1saW5rSm9pbiB7CiAgICAgIG5hbWUgPSAiY29udGVudHMiOwogICAgICBwYXRocyA9IFsgZmhzIF07CiAgICB9OwogICAgY29uZmlnID0gewogICAgICBFbnYgPSBbCiAgICAgICAgIlBBVEg9L2JpbiIKICAgICAgICAiU1NMX0NFUlRfRklMRT0ke3BrZ3MuY2FjZXJ0fS9ldGMvc3NsL2NlcnRzL2NhLWJ1bmRsZS5jcnQiCiAgICAgIF07CiAgICB9OwogIH07CmluCnBrZ3MucnVuQ29tbWFuZCAid3JpdGUtZG9ja2VyLXRhciIge30gJycKICAke3N0cmVhbX0gPiAkb3V0Cicn"
                              }
                            }
                          },
                          "target": {
                            "file": {
                              "path": "image.nix"
                            }
                          }
                        },
                        {
                          "source": {
                            "thunk": {
                              "thunk": {
                                "image": {
                                  "thunk": {
                                    "image": {
                                      "thunk": {
                                        "image": {
                                          "thunk": {
                                            "image": {
                                              "thunk": {
                                                "image": {
                                                  "ref": {
                                                    "platform": {
                                                      "os": "linux",
                                                      "arch": "amd64"
                                                    },
                                                    "repository": "alpine/git",
                                                    "tag": "latest",
                                                    "digest": "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
                                                  }
                                                }
                                              }
                                            },
                                            "args": [
                                              {
                                                "string": {
                                                  "value": "git"
                                                }
                                              },
                                              {
                                                "string": {
                                                  "value": "clone"
                                                }
                                              },
                                              {
                                                "string": {
                                                  "value": "https://github.com/vito/tabs"
                                                }
                                              },
                                              {
                                                "dirPath": {
                                                  "path": "."
                                                }
                                              }
                                            ]
                                          }
                                        },
                                        "args": [
                                          {
                                            "string": {
                                              "value": "git"
                                            }
                                          },
                                          {
                                            "string": {
                                              "value": "fetch"
                                            }
                                          },
                                          {
                                            "string": {
                                              "value": "origin"
                                            }
                                          },
                                          {
                                            "string": {
                                              "value": "6ca3f15d70b739a7929886e20b61dbc4cbdc4e22"
                                            }
                                          }
                                        ]
                                      }
                                    },
                                    "args": [
                                      {
                                        "string": {
                                          "value": "git"
                                        }
                                      },
                                      {
                                        "string": {
                                          "value": "checkout"
                                        }
                                      },
                                      {
                                        "string": {
                                          "value": "6ca3f15d70b739a7929886e20b61dbc4cbdc4e22"
                                        }
                                      }
                                    ]
                                  }
                                },
                                "args": [
                                  {
                                    "string": {
                                      "value": "git"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "submodule"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "update"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "--init"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "--recursive"
                                    }
                                  }
                                ]
                              },
                              "path": {
                                "dir": {
                                  "path": "."
                                }
                              }
                            }
                          },
                          "target": {
                            "dir": {
                              "path": "/flake"
                            }
                          }
                        },
                        {
                          "source": {
                            "cache": {
                              "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                              "path": {
                                "dir": {
                                  "path": "."
                                }
                              }
                            }
                          },
                          "target": {
                            "dir": {
                              "path": "/nix"
                            }
                          }
                        }
                      ]
                    }
                  },
                  "args": [
                    {
                      "string": {
                        "value": "cp"
                      }
                    },
                    {
                      "string": {
                        "value": "-aL"
                      }
                    },
                    {
                      "filePath": {
                        "path": "result"
                      }
                    },
                    {
                      "filePath": {
                        "path": "image.tar"
                      }
                    }
                  ],
                  "mounts": [
                    {
                      "source": {
                        "cache": {
                          "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                          "path": {
                            "dir": {
                              "path": "."
                            }
                          }
                        }
                      },
                      "target": {
                        "dir": {
                          "path": "/nix"
                        }
                      }
                    }
                  ]
                },
                "path": {
                  "file": {
                    "path": "image.tar"
                  }
                }
              }
            }
          }
        }
      }
    },
    "args": [
      {
        "string": {
          "value": "wget"
        }
      },
      {
        "string": {
          "value": "https://example.com"
        }
      },
      {
        "string": {
          "value": "-O"
        }
      },
      {
        "filePath": {
          "path": "index.html"
        }
      }
    ]
  },
  "path": {
    "file": {
      "path": "index.html"
    }
  }
}

server mode

I'm not sure if this is the right design for this yet, but it seems nifty and it works. Expect this to change at any moment. Suggestions welcome!

To serve Bass scripts in ./srv/ over HTTP on port 6455 ("bass"), run:

bass --serve 6455 ./srv/

This is particularly handy for cobbling together endpoints for receiving webhooks (e.g. a GitHub App for CI/CD).

HTTP requests sent to http://localhost:6455/foo will run the ./srv/foo Bass script.

The HTTP request sent on *stdin* as a structure like the following:

{:headers {:Accept "application/json"}
 :body "{\"foo\":1}"}

Values emitted to *stdout* will be sent as the response. If the script fails a 500 status code will be returned.

The UX here is very spartan at the moment. Notably there is no way to view progress over HTTP; it's only rendered server-side in the console.

I'd like the server-side to self-update somehow, but haven't figured that out yet.

webhooks based CI/CD

The Bass project uses Bass Loop to receive GitHub webhooks and run its own builds. Docs coming soon - see the announcement for now.

pipeline based CI/CD

Trigging builds on push is just one form of CI/CD. What if you have external dependencies you'd like to trigger builds from? What if you want to write sophisticated pipelines with fan-in and fan-out semantics?

Dunno yet! I think we're a few steps away from this, but we need to figure out the best steps.

Ideas for the future: