:image |
{
} |
||||||||||
:cmd |
.echo |
||||||||||
: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:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> echo "Hello, world!" [0.27s]▕ Hello, world!
null
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.02s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> sh -c "exit 1" ERROR [0.25s]!!! 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.
To influence caching, use with-label
to stamp thunks with arbitrary data. Two thunks that differ only in labels will be cached independently.
(run (with-label
(from (linux/alpine)
($ echo "Hello, world!"))
:foo "bar"))
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.22s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> echo "Hello, world!" [0.62s]▕ Hello, world!
null
(-> ($ echo "Hello, world!")
(with-image (linux/alpine))
(with-label :foo "bar")
run)
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.17s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.32s]=> echo "Hello, world!" [0.80s]▕ Hello, world!
null
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.08s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> cat [0.46s]=> exporting to client [0.00s]-> copying files 44B [0.00s]
(
"hello"
"goodbye"
end
)
To parse UNIX style tabular output, set the protocol to :unix-table
:
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.09s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.43s]=> ls -r /usr/bin [1.22s]=> exporting to client [0.00s]-> copying files 924B [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:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> echo "Hello, world!" [0.20s]=> exporting to client [0.00s]-> copying files 40B [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:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> cat /secret [0.15s]▕ 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
.
(def my-cache (cache-dir "my cache"))
(defn counter [tag]
(from (linux/alpine)
(-> ($ sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l")
(with-label :tag tag)
(with-mount my-cache /var/cache/))))
(defn count [tag]
(next (read (counter tag) :json)))
[(count "once")
(count "twice")
(count "thrice")]
=> sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l" [0.12s]=> sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l" [0.12s]=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l" [0.12s]=> exporting to client [0.00s]-> copying files 28B [0.00s]
(
1
1
1
)
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 |
{
} |
||||||||||
:cmd |
.sh |
||||||||||
:args |
(
) |
||||||||||
:stdin |
(
) |
}
./file
(-> ($ sh -c "cat > ./file")
(with-image (linux/alpine))
(with-stdin ["hello" "goodbye"])
(subpath ./file))
{
:image |
{
} |
||||||||||
:cmd |
.sh |
||||||||||
: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:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> sh -c "cat > ./file" [0.10s]=> exporting to client [0.00s]-> sending tarball [0.00s]
"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)
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> sh -c "cat > ./file" [0.14s]=> ls -al <thunk x_vZPyWs4P0=: (.sh)>/file [0.12s]▕ -rw-r--r-- 1 root root 18 Oct 26 1985 ./x_vZPyWs4P0=/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 |
{
} |
||||||||||||||||||||
:cmd |
.go |
||||||||||||||||||||
: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:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685 [0.01s]=> docker-image://docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685 CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685 [0.01s]=> git ls-remote https://github.com/vito/bass HEAD [0.33s]=> exporting to client [0.00s]-> copying files 72B [0.00s]
{
:image |
{
} |
||||||||||||||||||||||||||||
:cmd |
.git |
||||||||||||||||||||||||||||
:args |
(
) |
}
./
The .git
module also provides github
, a path root for repositories hosted at GitHub.
git:github/vito/bass/ref/HEAD/
{
:image |
{
} |
||||||||||||||||||||||||||||
:cmd |
.git |
||||||||||||||||||||||||||||
: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-label ($ 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)))
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.02s]=> sleep 1 [1.16s]=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" ERROR [0.17s]▕ hello▕ another line=> sleep 3 CANCELED [1.38s]!!! sh -c "echo \"$0\"; exit 1" "hello\nanother line"8: [0.15s] hello8: [0.15s] another lineerror! call trace (oldest first):┆ <fs>/multi-fail.bass:13:2..14:3712 │ (defn main []13 │ (ls (echo-sleep-exit "hello\nanother line" 1 1)14 │ (echo-sleep-exit "oh no" 3 42)))^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^┆ <fs>/multi-fail.bass:9:2..10:268 │ (defn ls paths9 │ (run (from (linux/alpine)10 │ ($ ls & $paths))))^^^^^^^^^^^^^^^^^^^^^^^^^build failed: exit code: 1run summary:=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c [0.01s]=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c=> sleep 3 [canceled] [1.38s]=> sleep 1 [1.16s]=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" [0.17s]helloanother lineERROR: exit code: 1=> sh -c "echo \"$0\"; exit 42" "oh no"=> ls <thunk 4BFQMgw3PwM=: (.sh)>/ <thunk Rl-wshJv2Nk=: (.sh)>/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": {
"ref": {
"platform": {
"os": "linux"
},
"repository": "golang",
"tag": "latest",
"digest": "sha256:a452d6273ad03a47c2f29b898d6bb57630e77baf839651ef77d03e4e049c5bf3"
}
},
"cmd": {
"command": {
"name": "go"
}
},
"args": [
{
"string": {
"value": "build"
}
},
{
"string": {
"value": "-o"
}
},
{
"dirPath": {
"path": "../out"
}
},
{
"string": {
"value": "./cmd/..."
}
}
],
"dir": {
"thunk": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux"
},
"repository": "alpine/git",
"tag": "latest",
"digest": "sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685"
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "clone"
}
},
{
"string": {
"value": "https://github.com/vito/bass"
}
},
{
"dirPath": {
"path": "."
}
}
]
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "fetch"
}
},
{
"string": {
"value": "origin"
}
},
{
"string": {
"value": "8bc8572e8bdb8856451ca8ea16f735b4a0aeb047"
}
}
]
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "checkout"
}
},
{
"string": {
"value": "8bc8572e8bdb8856451ca8ea16f735b4a0aeb047"
}
}
]
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"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": {
"ref": {
"platform": {
"os": "linux"
},
"repository": "ubuntu",
"tag": "latest",
"digest": "sha256:b6b83d3c331794420340093eb706a6f152d9c1fa51b262d9bf34594887c2c7ac"
}
},
"cmd": {
"command": {
"name": "apt-get"
}
},
"args": [
{
"string": {
"value": "update"
}
}
]
}
},
"cmd": {
"command": {
"name": "apt-get"
}
},
"args": [
{
"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:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685 [0.01s]=> docker-image://docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685 CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685 [0.01s]=> git ls-remote https://github.com/moby/buildkit HEAD [0.46s]=> exporting to client [0.00s]-> copying files 72B [0.00s]
"7b8733f64707a7248c5fff48a7ea08514c3ef02e"
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")
"7b8733f64707a7248c5fff48a7ea08514c3ef02e"
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.14s]=> resolve image config for docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae [0.01s]=> docker-image://docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae [0.01s]=> git ls-remote https://github.com/vito/bass main [0.39s]=> exporting to client [0.00s]-> copying files 83B [0.00s]
{
:image |
{
} |
||||||||||||||||||||||||||||
:cmd |
.git |
||||||||||||||||||||||||||||
: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:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae [0.01s]=> docker-image://docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae CACHED [0.00s]-> resolve docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae [0.01s]=> git ls-remote https://github.com/vito/tabs main [0.37s]=> exporting to client [0.00s]-> copying files 83B [0.00s]
{
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux"
},
"file": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux"
},
"repository": "nixos/nix",
"tag": "latest",
"digest": "sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6"
}
},
"cmd": {
"command": {
"name": "cp"
}
},
"args": [
{
"string": {
"value": "-anT"
}
},
{
"dirPath": {
"path": "/nix"
}
},
{
"dirPath": {
"path": "/cache"
}
}
],
"mounts": [
{
"source": {
"cache": {
"id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
"path": {
"dir": {
"path": "."
}
}
}
},
"target": {
"dir": {
"path": "/cache"
}
}
}
]
}
},
"cmd": {
"command": {
"name": "sh"
}
},
"args": [
{
"string": {
"value": "-c"
}
},
{
"string": {
"value": "echo accept-flake-config = true >> /etc/nix/nix.conf"
}
}
],
"mounts": [
{
"source": {
"cache": {
"id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
"path": {
"dir": {
"path": "."
}
}
}
},
"target": {
"dir": {
"path": "/nix"
}
}
}
]
}
},
"cmd": {
"command": {
"name": "sh"
}
},
"args": [
{
"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:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
"path": {
"dir": {
"path": "."
}
}
}
},
"target": {
"dir": {
"path": "/nix"
}
}
}
]
}
},
"cmd": {
"command": {
"name": "nix"
}
},
"args": [
{
"string": {
"value": "build"
}
},
{
"string": {
"value": ".#wget"
}
}
],
"mounts": [
{
"source": {
"thunk": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux"
},
"repository": "alpine/git",
"tag": "latest",
"digest": "sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae"
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "clone"
}
},
{
"string": {
"value": "https://github.com/vito/tabs"
}
},
{
"dirPath": {
"path": "."
}
}
]
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "fetch"
}
},
{
"string": {
"value": "origin"
}
},
{
"string": {
"value": "b3109bfbd35fd036bb53872e5d96efbe4e70d63c"
}
}
]
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "checkout"
}
},
{
"string": {
"value": "b3109bfbd35fd036bb53872e5d96efbe4e70d63c"
}
}
]
}
},
"cmd": {
"command": {
"name": "git"
}
},
"args": [
{
"string": {
"value": "submodule"
}
},
{
"string": {
"value": "update"
}
},
{
"string": {
"value": "--init"
}
},
{
"string": {
"value": "--recursive"
}
}
]
},
"path": {
"dir": {
"path": "."
}
}
}
},
"target": {
"dir": {
"path": "."
}
}
},
{
"source": {
"cache": {
"id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
"path": {
"dir": {
"path": "."
}
}
}
},
"target": {
"dir": {
"path": "/nix"
}
}
}
]
}
},
"cmd": {
"command": {
"name": "cp"
}
},
"args": [
{
"string": {
"value": "-aL"
}
},
{
"filePath": {
"path": "result"
}
},
{
"filePath": {
"path": "image.tar"
}
}
],
"mounts": [
{
"source": {
"cache": {
"id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
"path": {
"dir": {
"path": "."
}
}
}
},
"target": {
"dir": {
"path": "/nix"
}
}
}
]
},
"path": {
"file": {
"path": "image.tar"
}
}
},
"tag": "latest"
}
},
"cmd": {
"command": {
"name": "wget"
}
},
"args": [
{
"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.