:image |
{
} |
||||||||||||||
:args |
(
) |
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.
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.
Bass is built around thunks. Thunks are cacheable commands that produce files and/or a stream of values.
Throughout this documentation, thunks will be rendered as space invaders to make them easier to identify.
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.00s]=> echo "Hello, world!" CACHED [0.00s]
null
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.00s]=> sh -c "exit 1" ERROR [0.19s]!!! 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.
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> echo "Hi again!" [0.18s]▕ Hi again!
null
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.00s]=> .cat [0.19s]▕ "hello"▕ "goodbye"
(
"hello"
"goodbye"
end
)
To read output line-by-line, set the protocol to :lines
:
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> ls -r /usr/bin [0.18s]▕ 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▕ 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▕ blkdiscard▕ beep▕ bc▕ basename▕ awk▕ [[▕ [
"yes"
To parse UNIX style tabular output, set the protocol to :unix-table
:
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> ls -r /usr/bin CACHED [0.00s]
(
"yes"
)
To collect all output into one big string, set the protocol to :raw
:
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.00s]=> echo "Hello, world!" CACHED [0.00s]
"Hello, world!\n"
(mask "hunter2" :nickserv)
<secret: nickserv (7 bytes)>
Secrets can be passed to thunks as regular strings. When serialized, a secret's value is omitted.
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)
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> cat /secret [0.21s]▕ 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!
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
.
=> sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" once [0.19s]▕ 7=> sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" twice [0.21s]▕ 8=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" thrice [0.18s]▕ 9
(
7
8
9
)
Currently only one thunk can access a cache path at a time. This may become configurable in the future.
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:
{
:image |
{
} |
||||||||||||||
:args |
(
) |
||||||||||||||
:stdin |
(
) |
}
./file
(-> ($ sh -c "cat > ./file")
(with-image (linux/alpine))
(with-stdin ["hello" "goodbye"])
(subpath ./file))
{
:image |
{
} |
||||||||||||||
:args |
(
) |
||||||||||||||
:stdin |
(
) |
}
./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:
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> sh -c "cat > ./file" [0.22s]
"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)
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> sh -c "cat > ./file" [0.22s]=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> ls -al {{thunk ITFTJGPTH9SDQ: sh -c "cat > ./file"}}/file [0.23s]▕ -rw-r--r-- 1 root root 18 Oct 26 1985 ./ITFTJGPTH9SDQ/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.
{
:image |
{
} |
||||||||||||||||||||||
:args |
(
) |
||||||||||||||||||||||
:mounts |
(
) |
}
./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.
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.02s]=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.01s]=> git ls-remote https://github.com/vito/bass HEAD [0.72s]▕ 0537fa0323f1b12a43f0ab7dc90d2d00894d9c63 HEAD
{
:image |
{
} |
||||||||||||||||||||||||||
:args |
(
) |
}
./
The .git
module also provides github
, a path root for repositories hosted at GitHub.
git:github/vito/bass/ref/HEAD/
{
:image |
{
} |
||||||||||||||||||||||||||
:args |
(
) |
}
./
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)))
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> sleep 1 [1.25s]=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" ERROR [0.23s]▕ hello▕ another line=> sleep 3 CANCELED [1.56s]!!! sh -c "echo \"$0\"; exit 1" "hello\nanother line"4: [0.16s] hello4: [0.16s] another lineerror! call trace (oldest first):┆ <fs>/multi-fail.bass:15:2..16:3714 │ (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:2610 │ (defn ls paths11 │ (run (from (linux/alpine)12 │ ($ ls & $paths))))^^^^^^^^^^^^^^^^^^^^^^^^^resolve failed: exit code: 1run summary:=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126=> sleep 1 [1.25s]=> sleep 3 [canceled] [1.56s]=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" [0.23s]helloanother lineERROR: exit code: 1=> sh -c "echo \"$0\"; exit 42" "oh no"=> ls {{thunk 4G7OBEL2GH0SI: sh -c "echo \"$0\"; exit 1" "hello\nanother line"}}/ {{thunk 585RSM82KR2VE: sh -c "echo \"$0\"; exit 42" "oh no"}}/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.
Thunk paths can be saved in JSON format for archival, auditing, efficient distribution, or just for funsies.
{
"thunk": {
"image": {
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux",
"arch": "amd64"
},
"repository": "golang",
"tag": "latest",
"digest": "sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae44974a0c9445be331e72ff5e"
}
}
}
},
"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:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
}
}
},
"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": "0537fa0323f1b12a43f0ab7dc90d2d00894d9c63"
}
}
]
}
},
"args": [
{
"string": {
"value": "git"
}
},
{
"string": {
"value": "checkout"
}
},
{
"string": {
"value": "0537fa0323f1b12a43f0ab7dc90d2d00894d9c63"
}
}
]
}
},
"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.
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.
{
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux",
"arch": "amd64"
},
"repository": "ubuntu",
"tag": "latest",
"digest": "sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21"
}
}
}
},
"args": [
{
"string": {
"value": "apt-get"
}
},
{
"string": {
"value": "update"
}
}
]
}
},
"args": [
{
"string": {
"value": "apt-get"
}
},
{
"string": {
"value": "-y"
}
},
{
"string": {
"value": "install"
}
},
{
"string": {
"value": "git"
}
}
]
}
null
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-ls-remote "https://github.com/moby/buildkit" "HEAD")
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.02s]=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.01s]=> git ls-remote https://github.com/moby/buildkit HEAD [0.58s]▕ f1f27537acc758d10765876fca9e5270c674de55 HEAD
"f1f27537acc758d10765876fca9e5270c674de55"
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")
"f1f27537acc758d10765876fca9e5270c674de55"
Use bass --bump
to refresh every dependency in a bass.lock
file:
bass --bump bass.lock
The bass --bump
command re-load
s 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*
=> resolve image config for docker.io/alpine/git:latest [0.15s]=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.02s]=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.01s]=> git ls-remote https://github.com/vito/bass main [0.52s]▕ 0537fa0323f1b12a43f0ab7dc90d2d00894d9c63 refs/heads/main
{
:image |
{
} |
||||||||||||||||||||||||||
:args |
(
) |
}
./
Using bass.lock files lets you share and reuse Bass code in git
repos:
bass demos/git-lib.bass
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.00s]=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.00s]-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2 [0.00s]=> git fetch origin 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 CACHED [0.00s]▕ From https://github.com/vito/tabs▕ * branch 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 -> FETCH_HEAD=> git checkout 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 CACHED [0.00s]▕ 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=> git clone https://github.com/vito/tabs ./ CACHED [0.00s]▕ Cloning into '.'...=> git submodule update --init --recursive CACHED [0.00s]
{
"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"
}
}
}
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.
The Bass project uses Bass Loop to receive GitHub webhooks and run its own builds. Docs coming soon - see the announcement for now.
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:
The existing streams/pipes concepts could probably be leveraged for representing general-purpose concurrency.
It's possible to use streams to model Concourse style pipelines with the same constraint algorithm for passing sets of versions between jobs.