Repository: grafana/tanka
Pull request #878 (2023-06-12 09:00:44)

Bump typeface-fira-mono from 0.0.72 to 1.1.13 in /docs
Bumps [typeface-fira-mono](https://github.com/KyleAMathews/typefaces) from 0.0.72 to 1.1.13.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/87cd172773ee7461944264cb555d1976c8a92240"><code>87cd172</code></a> v1.1.13</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/40d9431591c3a73d6d053555321d58f273619cbf"><code>40d9431</code></a> fix markdown</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/59e0e03f4eb0ffe2cc76193670414ecafd947ab2"><code>59e0e03</code></a> v1.1.12</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/f599b3fea23bf948e8d00a9d0ac2a069eeabc4cc"><code>f599b3f</code></a> try try again</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/70bfba245f91d29a8543ec0cf99a44a33dfc055b"><code>70bfba2</code></a> v1.1.11</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/c62fb16f31ee71b93e602cf6dc0b9e0257a37306"><code>c62fb16</code></a> reduced</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/b496508632c20cd2cae9da09f901d111056ba055"><code>b496508</code></a> v1.1.10</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/d44cccae01684cef025d002f579fe2910e0a523e"><code>d44ccca</code></a> try</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/9390723de85049543f3873db81cf71927a0856d7"><code>9390723</code></a> v1.1.9</li>
<li><a href="https://github.com/KyleAMathews/typefaces/commit/d7947f1e3b865ae8ab19cfce7324562375ac94a8"><code>d7947f1</code></a> try try again</li>
<li>Additional commits viewable in <a href="https://github.com/KyleAMathews/typefaces/compare/v0.0.72...v1.1.13">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typeface-fira-mono&package-manager=npm_and_yarn&previous-version=0.0.72&new-version=1.1.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
Dependabot will merge this PR once it's up-to-date and CI passes on it, as requested by @julienduchesne.

[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.


github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.4.4
:---:
:rocket: Deployed preview to https://grafana.github.io/tanka/pr-preview/pr-878/
on branch [`gh-pages`](https://github.com/grafana/tanka/tree/gh-pages) at 2023-06-12 09:04 UTC
<!-- Sticky Pull Request Commentpr-preview -->

Patch:
diff --git a/.drone/docker-manifest.tmpl b/.drone/docker-manifest.tmpl
deleted file mode 100644
index 2ec7d492..00000000
--- a/.drone/docker-manifest.tmpl
+++ /dev/null
@@ -1,22 +0,0 @@
-image: grafana/tanka:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
-{{#if build.tags}}
-tags:
-{{#each build.tags}}
-  - {{this}}
-{{/each}}
-{{/if}}
-manifests:
-  - image: grafana/tanka:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}amd64
-    platform:
-      architecture: amd64
-      os: linux
-  - image: grafana/tanka:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm64
-    platform:
-      architecture: arm64
-      os: linux
-      variant: v8
-  - image: grafana/tanka:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm
-    platform:
-      architecture: arm
-      os: linux
-      variant: v7
diff --git a/.drone/drone.jsonnet b/.drone/drone.jsonnet
deleted file mode 100644
index a2ecf75e..00000000
--- a/.drone/drone.jsonnet
+++ /dev/null
@@ -1,159 +0,0 @@
-local vault = import 'vault.libsonnet';
-
-local golang = 'golang:1.20';
-
-local volumes = [{ name: 'gopath', temp: {} }];
-local mounts = [{ name: 'gopath', path: '/go' }];
-
-local constraints = {
-  local withRef(ref) = {
-    trigger+: {
-      ref+: [ref],
-    },
-  },
-
-  tags: withRef('refs/tags/v*'),
-  mainPush: withRef('refs/heads/main'),
-  pullRequest: withRef('refs/pull/*/head'),
-};
-
-local go(name, commands) = {
-  name: name,
-  image: golang,
-  volumes: mounts,
-  commands: commands,
-};
-
-local make(target) = go(target, [
-  // Only download it once, then for every step, copy it to the right place.
-  'if [ ! -f linux-amd64/helm ]; then',
-  '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz',
-  '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz',
-  '  rm -f helm-v3.9.0-linux-amd64.tar.gz',
-  'fi',
-  'cp linux-amd64/helm /usr/local/bin/helm',
-  'make ' + target,
-]);
-
-local pipeline(name) = {
-  kind: 'pipeline',
-  name: name,
-  volumes: volumes,
-  steps: [],
-};
-
-local docker(arch, depends_on=[]) =
-  pipeline('docker-' + arch) {
-    platform: {
-      os: 'linux',
-      arch: arch,
-    },
-    steps: [
-      go('fetch-tags', ['git fetch origin --tags']),
-      make('static'),
-      {
-        name: 'container',
-        image: 'plugins/docker',
-        settings: {
-          repo: 'grafana/tanka',
-          auto_tag: true,
-          auto_tag_suffix: arch,
-          username: { from_secret: vault.dockerhub_username },
-          password: { from_secret: vault.dockerhub_password },
-        },
-      },
-    ],
-    depends_on: depends_on,
-  };
-
-[
-  pipeline('check') {
-    steps: [
-      go('download', ['go mod download']),
-      make('lint'),
-      make('test'),
-      make('cross') { name: 'build' },
-    ],
-  } + constraints.pullRequest + constraints.mainPush,
-
-  pipeline('benchmark against main') {
-    node: {
-      // To ensure that benchmarks are accurate, run this build on a node that doesn't do parallel builds.
-      type: 'no-parallel',
-    },
-    steps: [
-      go('benchmark', [
-        'go test -bench=. -benchmem -count=6 -run=^$ ./... | tee bench-pr.txt',
-        'git fetch origin main',
-        'git reset --hard origin/main',
-        'go test -bench=. -benchmem -count=6 -run=^$ ./... | tee bench-main.txt',
-        'go install golang.org/x/perf/cmd/...@latest',
-        'benchstat bench-main.txt bench-pr.txt',
-      ]),
-    ],
-  } + constraints.pullRequest,
-
-  pipeline('release') {
-    steps: [
-      go('fetch-tags', ['git fetch origin --tags']),
-      make('cross'),
-      {
-        name: 'publish',
-        image: 'plugins/github-release',
-        settings: {
-          title: '${DRONE_TAG}',
-          note: importstr 'release-note.md',
-          api_key: { from_secret: vault.grafanabot_public_account_token },
-          files: 'dist/*',
-          draft: true,
-        },
-      },
-    ],
-  } + { depends_on: ['check'] } + constraints.tags,
-
-  docker('amd64', depends_on=['check']) + constraints.tags + constraints.mainPush,
-  docker('arm64', depends_on=['check']) + constraints.tags + constraints.mainPush,
-
-  pipeline('manifest-main') {
-    steps: [
-      go('fetch-tags', [
-        'git fetch origin --tags',
-        'echo "main-$(git describe --tags)" > .tags',
-      ]),
-      {
-        name: 'manifest',
-        image: 'plugins/manifest:1.4.0',
-        settings: {
-          ignore_missing: true,
-          spec: '.drone/docker-manifest.tmpl',
-          username: { from_secret: vault.dockerhub_username },
-          password: { from_secret: vault.dockerhub_password },
-        },
-      },
-    ],
-  } + {
-    depends_on: [
-      'docker-amd64',
-      'docker-arm64',
-    ],
-  } + constraints.mainPush,
-
-  pipeline('manifest') {
-    steps: [{
-      name: 'manifest',
-      image: 'plugins/manifest:1.4.0',
-      settings: {
-        auto_tag: true,
-        ignore_missing: true,
-        spec: '.drone/docker-manifest.tmpl',
-        username: { from_secret: vault.dockerhub_username },
-        password: { from_secret: vault.dockerhub_password },
-      },
-    }],
-  } + {
-    depends_on: [
-      'docker-amd64',
-      'docker-arm64',
-    ],
-  } + constraints.tags + constraints.mainPush,
-] + vault.secrets
diff --git a/.drone/drone.yml b/.drone/drone.yml
deleted file mode 100644
index 16ba2dda..00000000
--- a/.drone/drone.yml
+++ /dev/null
@@ -1,311 +0,0 @@
----
-kind: pipeline
-name: check
-steps:
-- commands:
-  - go mod download
-  image: golang:1.20
-  name: download
-  volumes:
-  - name: gopath
-    path: /go
-- commands:
-  - if [ ! -f linux-amd64/helm ]; then
-  - '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz'
-  - '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz'
-  - '  rm -f helm-v3.9.0-linux-amd64.tar.gz'
-  - fi
-  - cp linux-amd64/helm /usr/local/bin/helm
-  - make lint
-  image: golang:1.20
-  name: lint
-  volumes:
-  - name: gopath
-    path: /go
-- commands:
-  - if [ ! -f linux-amd64/helm ]; then
-  - '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz'
-  - '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz'
-  - '  rm -f helm-v3.9.0-linux-amd64.tar.gz'
-  - fi
-  - cp linux-amd64/helm /usr/local/bin/helm
-  - make test
-  image: golang:1.20
-  name: test
-  volumes:
-  - name: gopath
-    path: /go
-- commands:
-  - if [ ! -f linux-amd64/helm ]; then
-  - '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz'
-  - '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz'
-  - '  rm -f helm-v3.9.0-linux-amd64.tar.gz'
-  - fi
-  - cp linux-amd64/helm /usr/local/bin/helm
-  - make cross
-  image: golang:1.20
-  name: build
-  volumes:
-  - name: gopath
-    path: /go
-trigger:
-  ref:
-  - refs/pull/*/head
-  - refs/heads/main
-volumes:
-- name: gopath
-  temp: {}
----
-kind: pipeline
-name: benchmark against main
-node:
-  type: no-parallel
-steps:
-- commands:
-  - go test -bench=. -benchmem -count=6 -run=^$ ./... | tee bench-pr.txt
-  - git fetch origin main
-  - git reset --hard origin/main
-  - go test -bench=. -benchmem -count=6 -run=^$ ./... | tee bench-main.txt
-  - go install golang.org/x/perf/cmd/...@latest
-  - benchstat bench-main.txt bench-pr.txt
-  image: golang:1.20
-  name: benchmark
-  volumes:
-  - name: gopath
-    path: /go
-trigger:
-  ref:
-  - refs/pull/*/head
-volumes:
-- name: gopath
-  temp: {}
----
-depends_on:
-- check
-kind: pipeline
-name: release
-steps:
-- commands:
-  - git fetch origin --tags
-  image: golang:1.20
-  name: fetch-tags
-  volumes:
-  - name: gopath
-    path: /go
-- commands:
-  - if [ ! -f linux-amd64/helm ]; then
-  - '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz'
-  - '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz'
-  - '  rm -f helm-v3.9.0-linux-amd64.tar.gz'
-  - fi
-  - cp linux-amd64/helm /usr/local/bin/helm
-  - make cross
-  image: golang:1.20
-  name: cross
-  volumes:
-  - name: gopath
-    path: /go
-- image: plugins/github-release
-  name: publish
-  settings:
-    api_key:
-      from_secret: grafanabot_pat
-    draft: true
-    files: dist/*
-    note: |
-      This is release ${DRONE_TAG} of Tanka (`tk`). Check out the [CHANGELOG](https://github.com/grafana/tanka/blob/main/CHANGELOG.md) for detailed release notes.
-      ## Install instructions
-
-      #### Binary:
-      ```bash
-      # download the binary (adapt os and arch as needed)
-      $ curl -fSL -o "/usr/local/bin/tk" "https://github.com/grafana/tanka/releases/download/${DRONE_TAG}/tk-linux-amd64"
-
-      # make it executable
-      $ chmod a+x "/usr/local/bin/tk"
-
-      # have fun :)
-      $ tk --help
-      ```
-
-      #### Docker container:
-      https://hub.docker.com/r/grafana/tanka
-      ```bash
-      $ docker pull grafana/tanka:${DRONE_TAG#v}
-      ```
-    title: ${DRONE_TAG}
-trigger:
-  ref:
-  - refs/tags/v*
-volumes:
-- name: gopath
-  temp: {}
----
-depends_on:
-- check
-kind: pipeline
-name: docker-amd64
-platform:
-  arch: amd64
-  os: linux
-steps:
-- commands:
-  - git fetch origin --tags
-  image: golang:1.20
-  name: fetch-tags
-  volumes:
-  - name: gopath
-    path: /go
-- commands:
-  - if [ ! -f linux-amd64/helm ]; then
-  - '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz'
-  - '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz'
-  - '  rm -f helm-v3.9.0-linux-amd64.tar.gz'
-  - fi
-  - cp linux-amd64/helm /usr/local/bin/helm
-  - make static
-  image: golang:1.20
-  name: static
-  volumes:
-  - name: gopath
-    path: /go
-- image: plugins/docker
-  name: container
-  settings:
-    auto_tag: true
-    auto_tag_suffix: amd64
-    password:
-      from_secret: dockerhub_password
-    repo: grafana/tanka
-    username:
-      from_secret: dockerhub_username
-trigger:
-  ref:
-  - refs/tags/v*
-  - refs/heads/main
-volumes:
-- name: gopath
-  temp: {}
----
-depends_on:
-- check
-kind: pipeline
-name: docker-arm64
-platform:
-  arch: arm64
-  os: linux
-steps:
-- commands:
-  - git fetch origin --tags
-  image: golang:1.20
-  name: fetch-tags
-  volumes:
-  - name: gopath
-    path: /go
-- commands:
-  - if [ ! -f linux-amd64/helm ]; then
-  - '  wget -q https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz'
-  - '  tar -zxvf helm-v3.9.0-linux-amd64.tar.gz'
-  - '  rm -f helm-v3.9.0-linux-amd64.tar.gz'
-  - fi
-  - cp linux-amd64/helm /usr/local/bin/helm
-  - make static
-  image: golang:1.20
-  name: static
-  volumes:
-  - name: gopath
-    path: /go
-- image: plugins/docker
-  name: container
-  settings:
-    auto_tag: true
-    auto_tag_suffix: arm64
-    password:
-      from_secret: dockerhub_password
-    repo: grafana/tanka
-    username:
-      from_secret: dockerhub_username
-trigger:
-  ref:
-  - refs/tags/v*
-  - refs/heads/main
-volumes:
-- name: gopath
-  temp: {}
----
-depends_on:
-- docker-amd64
-- docker-arm64
-kind: pipeline
-name: manifest-main
-steps:
-- commands:
-  - git fetch origin --tags
-  - echo "main-$(git describe --tags)" > .tags
-  image: golang:1.20
-  name: fetch-tags
-  volumes:
-  - name: gopath
-    path: /go
-- image: plugins/manifest:1.4.0
-  name: manifest
-  settings:
-    ignore_missing: true
-    password:
-      from_secret: dockerhub_password
-    spec: .drone/docker-manifest.tmpl
-    username:
-      from_secret: dockerhub_username
-trigger:
-  ref:
-  - refs/heads/main
-volumes:
-- name: gopath
-  temp: {}
----
-depends_on:
-- docker-amd64
-- docker-arm64
-kind: pipeline
-name: manifest
-steps:
-- image: plugins/manifest:1.4.0
-  name: manifest
-  settings:
-    auto_tag: true
-    ignore_missing: true
-    password:
-      from_secret: dockerhub_password
-    spec: .drone/docker-manifest.tmpl
-    username:
-      from_secret: dockerhub_username
-trigger:
-  ref:
-  - refs/tags/v*
-  - refs/heads/main
-volumes:
-- name: gopath
-  temp: {}
----
-get:
-  name: pat
-  path: infra/data/ci/github/grafanabot
-kind: secret
-name: grafanabot_pat
----
-get:
-  name: username
-  path: infra/data/ci/docker_hub
-kind: secret
-name: dockerhub_username
----
-get:
-  name: password
-  path: infra/data/ci/docker_hub
-kind: secret
-name: dockerhub_password
----
-kind: signature
-hmac: f0dfa2a5202af05dbfca14e09d1c66222bf6d670d77b5f41b66d696985e89fb7
-
-...
diff --git a/.drone/release-note.md b/.drone/release-note.md
deleted file mode 100644
index 3c9d47e7..00000000
--- a/.drone/release-note.md
+++ /dev/null
@@ -1,20 +0,0 @@
-This is release ${DRONE_TAG} of Tanka (`tk`). Check out the [CHANGELOG](https://github.com/grafana/tanka/blob/main/CHANGELOG.md) for detailed release notes.
-## Install instructions
-
-#### Binary:
-```bash
-# download the binary (adapt os and arch as needed)
-$ curl -fSL -o "/usr/local/bin/tk" "https://github.com/grafana/tanka/releases/download/${DRONE_TAG}/tk-linux-amd64"
-
-# make it executable
-$ chmod a+x "/usr/local/bin/tk"
-
-# have fun :)
-$ tk --help
-```
-
-#### Docker container:
-https://hub.docker.com/r/grafana/tanka
-```bash
-$ docker pull grafana/tanka:${DRONE_TAG#v}
-```
diff --git a/.drone/vault.libsonnet b/.drone/vault.libsonnet
deleted file mode 100644
index ce08d436..00000000
--- a/.drone/vault.libsonnet
+++ /dev/null
@@ -1,20 +0,0 @@
-local secret(name, vault_path, key) = {
-  kind: 'secret',
-  name: name,
-  get: {
-    path: vault_path,
-    name: key,
-  },
-};
-
-{
-  dockerhub_username: 'dockerhub_username',
-  dockerhub_password: 'dockerhub_password',
-  grafanabot_public_account_token: 'grafanabot_pat',
-
-  secrets: [
-    secret($.grafanabot_public_account_token, 'infra/data/ci/github/grafanabot', 'pat'),
-    secret($.dockerhub_username, 'infra/data/ci/docker_hub', 'username'),
-    secret($.dockerhub_password, 'infra/data/ci/docker_hub', 'password'),
-  ],
-}
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 00000000..b0fbe9c6
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,136 @@
+# See https://docs.docker.com/build/ci/github-actions/multi-platform/
+name: Docker
+
+on:
+  push:
+    branches:
+      - main
+    tags:
+      - v*
+  pull_request:
+    branches:
+      - '*'
+
+permissions:
+  contents: read
+  id-token: write
+
+env:
+  REGISTRY_IMAGE: grafana/tanka
+  # Docker image tags. See https://github.com/docker/metadata-action for format
+  TAGS_CONFIG: |
+    type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
+    type=sha,prefix={{branch}}-,format=short,enable=${{ github.ref == 'refs/heads/main' }}
+    type=semver,pattern={{version}}
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        platform:
+          - linux/amd64
+          - linux/arm64
+    
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: ${{ env.REGISTRY_IMAGE }}
+          tags: ${{ env.TAGS_CONFIG }}
+
+      # Setup buildx
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      # Login to Docker Hub
+      - name: Get Secrets
+        if: github.event_name != 'pull_request'
+        uses: grafana/shared-workflows/actions/get-vault-secrets@main
+        with:
+          # Secrets placed in the ci/common/<path> path in Vault
+          common_secrets: |
+            DOCKERHUB_USERNAME=dockerhub:username
+            DOCKERHUB_TOKEN=dockerhub:password
+
+      - name: Login to Docker Hub
+        if: github.event_name != 'pull_request'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ env.DOCKERHUB_USERNAME }}
+          password: ${{ env.DOCKERHUB_TOKEN }}
+  
+      - name: Build and push by digest
+        id: build
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          platforms: ${{ matrix.platform }}
+          labels: ${{ steps.meta.outputs.labels }}
+          outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
+
+      - name: Export digest
+        if: github.event_name != 'pull_request'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ steps.build.outputs.digest }}"
+          touch "/tmp/digests/${digest#sha256:}"          
+      - name: Upload digest
+        if: github.event_name != 'pull_request'
+        uses: actions/upload-artifact@v3
+        with:
+          name: digests
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+  
+  merge:
+    runs-on: ubuntu-latest
+    if: github.event_name != 'pull_request'
+    needs: 
+      - build
+    steps:
+      - name: Download digests
+        uses: actions/download-artifact@v3
+        with:
+          name: digests
+          path: /tmp/digests
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+    
+      - name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: ${{ env.REGISTRY_IMAGE }}
+          tags: ${{ env.TAGS_CONFIG }}
+
+      # Login to Docker Hub
+      - name: Get Secrets
+        uses: grafana/shared-workflows/actions/get-vault-secrets@main
+        with:
+          # Secrets placed in the ci/common/<path> path in Vault
+          common_secrets: |
+            DOCKERHUB_USERNAME=dockerhub:username
+            DOCKERHUB_TOKEN=dockerhub:password
+      - name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ env.DOCKERHUB_USERNAME }}
+          password: ${{ env.DOCKERHUB_TOKEN }}
+
+      - name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
+            $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)          
+      - name: Inspect image
+        run: |
+          docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}          
diff --git a/.github/workflows/publish-page.yml b/.github/workflows/publish-page.yml
index 5bf0fea2..3bad19bf 100644
--- a/.github/workflows/publish-page.yml
+++ b/.github/workflows/publish-page.yml
@@ -23,9 +23,9 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: 16
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..4b62a4ff
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,50 @@
+name: Release
+
+on:
+  push:
+    tags:
+      - v*
+
+permissions:
+  contents: write
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-tags: true
+      - uses: actions/setup-go@v4
+        with:
+          go-version: '1.20'
+      - run: make cross
+      - id: docker_tag
+        run: echo "DOCKER_TAG=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
+      - name: Release
+        uses: softprops/action-gh-release@v1
+        with:
+          body: |
+            This is release `${{ env.GITHUB_REF_NAME }}` of Tanka (`tk`). Check out the [CHANGELOG](https://github.com/grafana/tanka/blob/main/CHANGELOG.md) for detailed release notes.
+            ## Install instructions
+
+            #### Binary:
+            ```bash
+            # download the binary (adapt os and arch as needed)
+            $ curl -fSL -o "/usr/local/bin/tk" "https://github.com/grafana/tanka/releases/download/${{ env.GITHUB_REF_NAME }}/tk-linux-amd64"
+
+            # make it executable
+            $ chmod a+x "/usr/local/bin/tk"
+
+            # have fun :)
+            $ tk --help
+            ```
+
+            #### Docker container:
+            https://hub.docker.com/r/grafana/tanka
+            ```bash
+            $ docker pull grafana/tanka:${{ env.DOCKER_TAG }}
+            ```
+          draft: true
+          files: |
+            dist/*
diff --git a/Dockerfile b/Dockerfile
index 6d6df6e9..d290ff98 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # download kubectl
-FROM golang:1.21.0-alpine as kubectl
+FROM golang:1.21.3-alpine as kubectl
 RUN apk add --no-cache curl
 RUN export VERSION=$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt) &&\
     export OS=$(go env GOOS) && \
@@ -8,7 +8,7 @@ RUN export VERSION=$(curl -s https://storage.googleapis.com/kubernetes-release/r
     chmod +x /usr/local/bin/kubectl
 
 # build jsonnet-bundler
-FROM golang:1.21.0-alpine as jb
+FROM golang:1.21.3-alpine as jb
 WORKDIR /tmp
 RUN apk add --no-cache git make bash &&\
     git clone https://github.com/jsonnet-bundler/jsonnet-bundler &&\
@@ -17,7 +17,7 @@ RUN apk add --no-cache git make bash &&\
     make static &&\
     mv _output/jb /usr/local/bin/jb
 
-FROM golang:1.21.0-alpine as helm
+FROM golang:1.21.3-alpine as helm
 WORKDIR /tmp/helm
 RUN apk add --no-cache jq curl
 RUN export TAG=$(curl --silent "https://api.github.com/repos/helm/helm/releases/latest" | jq -r .tag_name) &&\
@@ -26,7 +26,7 @@ RUN export TAG=$(curl --silent "https://api.github.com/repos/helm/helm/releases/
     curl -SL "https://get.helm.sh/helm-${TAG}-${OS}-${ARCH}.tar.gz" > helm.tgz && \
     tar -xvf helm.tgz --strip-components=1
 
-FROM golang:1.21.0-alpine as kustomize
+FROM golang:1.21.3-alpine as kustomize
 WORKDIR /tmp/kustomize
 RUN apk add --no-cache jq curl
 # Get the latest version of kustomize
@@ -39,10 +39,15 @@ RUN export TAG=$(curl --silent "https://api.github.com/repos/kubernetes-sigs/kus
     curl -SL "https://github.com/kubernetes-sigs/kustomize/releases/download/${TAG}/kustomize_${VERSION_TAG}_${OS}_${ARCH}.tar.gz" > kustomize.tgz && \
     tar -xvf kustomize.tgz
 
+FROM golang:1.21.3 as build
+WORKDIR /app
+COPY . .
+RUN make static
+
 # assemble final container
 FROM alpine:3.18
 RUN apk add --no-cache coreutils diffutils less git openssh-client
-COPY tk /usr/local/bin/tk
+COPY --from=build /app/tk /usr/local/bin/tk
 COPY --from=kubectl /usr/local/bin/kubectl /usr/local/bin/kubectl
 COPY --from=jb /usr/local/bin/jb /usr/local/bin/jb
 COPY --from=helm /tmp/helm/helm /usr/local/bin/helm
diff --git a/Makefile b/Makefile
index 4dc47767..86c32d6e 100644
--- a/Makefile
+++ b/Makefile
@@ -38,9 +38,3 @@ cross: $(GOX)
 # Docker container
 container: static
 	docker build -t grafana/tanka .
-
-# CI
-drone:
-	drone jsonnet --source .drone/drone.jsonnet --target .drone/drone.yml --stream --format
-	drone lint .drone/drone.yml
-	drone sign --save grafana/tanka .drone/drone.yml
diff --git a/README.md b/README.md
index e731de5e..94177345 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,6 @@
 </p>
 
 <p align="center">
-  <a href="https://cloud.drone.io/grafana/tanka">
-    <img src="https://img.shields.io/drone/build/grafana/tanka?style=flat-square&server=https%3A%2F%2Fdrone.grafana.net">
-  </a>
   <a href="https://github.com/grafana/tanka/releases">
     <img src="https://img.shields.io/github/release/grafana/tanka?style=flat-square" />
   </a>
diff --git a/cmd/tk/export.go b/cmd/tk/export.go
index 31f3236c..f3d822e7 100644
--- a/cmd/tk/export.go
+++ b/cmd/tk/export.go
@@ -86,26 +86,27 @@ func exportCmd() *cli.Command {
 		}
 
 		var exportEnvs []*v1alpha1.Environment
-		for _, path := range args[1:] {
-			// find possible environments
-			if *recursive {
-				// get absolute path to Environment
-				envs, err := tanka.FindEnvs(path, tanka.FindOpts{Selector: opts.Selector})
-				if err != nil {
-					return err
-				}
+		// find possible environments
+		if *recursive {
+			// get absolute path to Environment
+			envs, err := tanka.FindEnvsFromPaths(args[1:], tanka.FindOpts{Selector: opts.Selector, Parallelism: opts.Parallelism})
+			if err != nil {
+				return err
+			}
 
-				for _, env := range envs {
-					if opts.Opts.Name != "" && opts.Opts.Name != env.Metadata.Name {
-						continue
-					}
-					exportEnvs = append(exportEnvs, env)
+			for _, env := range envs {
+				if opts.Opts.Name != "" && opts.Opts.Name != env.Metadata.Name {
+					continue
 				}
-				continue
+				exportEnvs = append(exportEnvs, env)
+			}
+		} else {
+			if len(args[1:]) > 1 {
+				return fmt.Errorf("recursive flag is required when exporting multiple environments")
 			}
 
 			// validate environment
-			env, err := tanka.Peek(path, opts.Opts)
+			env, err := tanka.Peek(args[1], opts.Opts)
 			if err != nil {
 				switch err.(type) {
 				case tanka.ErrMultipleEnvs:
diff --git a/cmd/tk/flags.go b/cmd/tk/flags.go
index ad2715ab..79e43627 100644
--- a/cmd/tk/flags.go
+++ b/cmd/tk/flags.go
@@ -22,7 +22,7 @@ func workflowFlags(fs *pflag.FlagSet) *workflowFlagVars {
 	v := workflowFlagVars{}
 	fs.StringVar(&v.name, "name", "", "string that only a single inline environment contains in its name")
 	fs.StringSliceVarP(&v.targets, "target", "t", nil, "Regex filter on '<kind>/<name>'. See https://tanka.dev/output-filtering")
-	fs.StringVar(&v.jsonnetImplementation, "jsonnet-implementation", "go", "Use `go` to use native go-jsonnet implementation and `binary:<path>` to delegate evaluation to a binary (with the same API as the regular `jsonnet` binary)")
+	fs.StringVar(&v.jsonnetImplementation, "jsonnet-implementation", "go", "Use `go` to use native go-jsonnet implementation and `binary:<path>` to delegate evaluation to a binary (with the same API as the regular `jsonnet` binary, see the BinaryImplementation docstrings for more details)")
 	return &v
 }
 
diff --git a/docs/docs/kustomize.mdx b/docs/docs/kustomize.mdx
index 67a3f703..1f1aaf47 100644
--- a/docs/docs/kustomize.mdx
+++ b/docs/docs/kustomize.mdx
@@ -108,15 +108,14 @@ from. This most likely means you didn't call `new(std.thisFile)` when importing
 
 ```jsonnet
 local tanka = import "github.com/grafana/jsonnet-libs/tanka-util/main.libsonnet";
-local helm = tanka.helm.new(std.thisFile);
-                       ↑ This is important
+local kustomize = tanka.kustomize.new(std.thisFile);
+                                ↑ This is important
 ```
 
 ### Failed to find kustomization
 
 ```
-helmTemplate: Failed to find a kustomization at 'flux2': No such file or directory.
-helmTemplate: Failed to find a kustomization at '/home/user/stuff/tanka/environments/default/flux2': No such file or directory.
+Error: unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization' in directory '/home/user/stuff/tanka/environments/default/flux2'
 ```
 
 Tanka failed to locate your kustomization on the filesystem. It looked at the
diff --git a/docs/package.json b/docs/package.json
index b4e26ed2..8c7041ae 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -20,12 +20,12 @@
     "react": "^16.12.0",
     "react-dom": "^16.12.0",
     "react-helmet": "^6.1.0",
-    "typeface-fira-mono": "^0.0.72",
+    "typeface-fira-mono": "^1.1.13",
     "typeface-source-sans-pro": "^0.0.75",
     "typescript": "^4.9.5"
   },
   "devDependencies": {
-    "prettier": "^2.8.8"
+    "prettier": "^3.0.3"
   },
   "keywords": [
     "gatsby"
diff --git a/docs/yarn.lock b/docs/yarn.lock
index ef55c89e..d4e2b759 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -13265,11 +13265,16 @@ prettier@^1.18.2:
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
   integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
 
-prettier@^2.0.5, prettier@^2.8.8:
+prettier@^2.0.5:
   version "2.8.8"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
   integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
 
+prettier@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
+  integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
+
 pretty-bytes@^5.1.0, pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -16256,10 +16261,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
 
-typeface-fira-mono@^0.0.72:
-  version "0.0.72"
-  resolved "https://registry.yarnpkg.com/typeface-fira-mono/-/typeface-fira-mono-0.0.72.tgz#b396d2a08814bb3a32d94ef71c74d37883c3475b"
-  integrity sha512-bouzjtniGMrakhazipUl6mskswe7UvH3qynU5Wsl0O7NMuL178VtZ0pOqUGsjBJ42l64GZo9nUMI1eSK4kvJbQ==
+typeface-fira-mono@^1.1.13:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/typeface-fira-mono/-/typeface-fira-mono-1.1.13.tgz#753c3e6100e3620581b94f5deb86ce8f8ed3bda8"
+  integrity sha512-+1T4zpyB2kYoOP7K0+k4gSaxchkwh/UeOC30h1vstAetF4+eJc6qVI3adrFVOPB7sYwHXJtLcfkKG2fAMs3TgQ==
 
 typeface-source-sans-pro@^0.0.75:
   version "0.0.75"
diff --git a/go.mod b/go.mod
index e49c9144..618d993d 100644
--- a/go.mod
+++ b/go.mod
@@ -9,22 +9,22 @@ require (
 	github.com/fatih/structs v1.1.0
 	github.com/go-clix/cli v0.2.0
 	github.com/gobwas/glob v0.2.3
-	github.com/google/go-cmp v0.5.9
+	github.com/google/go-cmp v0.6.0
 	github.com/google/go-jsonnet v0.20.0
 	github.com/karrick/godirwalk v1.17.0
 	github.com/pkg/errors v0.9.1
 	github.com/posener/complete v1.2.3
 	github.com/rs/zerolog v1.30.0
 	github.com/spf13/pflag v1.0.5
-	github.com/stretchr/objx v0.5.0
+	github.com/stretchr/objx v0.5.1
 	github.com/stretchr/testify v1.8.4
 	github.com/thoas/go-funk v0.9.3
 	golang.org/x/term v0.11.0
-	golang.org/x/text v0.12.0
+	golang.org/x/text v0.13.0
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.1
-	k8s.io/apimachinery v0.27.3
-	k8s.io/utils v0.0.0-20230209194617-a36077c30491
+	k8s.io/apimachinery v0.28.3
+	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
 	sigs.k8s.io/yaml v1.3.0
 )
 
@@ -32,7 +32,7 @@ require (
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/go-logr/logr v1.2.3 // indirect
+	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -46,6 +46,6 @@ require (
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/spf13/cast v1.4.1 // indirect
 	golang.org/x/crypto v0.3.0 // indirect
-	golang.org/x/sys v0.11.0 // indirect
-	k8s.io/klog/v2 v2.90.1 // indirect
+	golang.org/x/sys v0.13.0 // indirect
+	k8s.io/klog/v2 v2.100.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 787316fe..95bf8c35 100644
--- a/go.sum
+++ b/go.sum
@@ -17,13 +17,13 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
 github.com/go-clix/cli v0.2.0 h1:rqpcyS/cvshOhXkwii0V+7nWetDVC8cp4pKI7JiCIS8=
 github.com/go-clix/cli v0.2.0/go.mod h1:yWI9abpv187r47lDjz8Z9TWev93aUTWaW2seSb5JmPQ=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
-github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g=
 github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -75,13 +75,15 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
+github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
@@ -107,8 +109,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -118,8 +120,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -133,11 +135,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
-k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
-k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
-k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
-k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
+k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
+k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
+k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
 sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/pkg/jsonnet/implementations/binary/impl.go b/pkg/jsonnet/implementations/binary/impl.go
new file mode 100644
index 00000000..30f37fd4
--- /dev/null
+++ b/pkg/jsonnet/implementations/binary/impl.go
@@ -0,0 +1,70 @@
+package binary
+
+import (
+	"fmt"
+	"os/exec"
+	"strconv"
+
+	"github.com/grafana/tanka/pkg/jsonnet/implementations/types"
+)
+
+type JsonnetBinaryRunner struct {
+	binPath string
+	args    []string
+}
+
+func (r *JsonnetBinaryRunner) EvaluateAnonymousSnippet(snippet string) (string, error) {
+	cmd := exec.Command(r.binPath, append(r.args, "-e", snippet)...)
+
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("error running anonymous snippet: %w\n%s", err, string(out))
+	}
+
+	return string(out), nil
+}
+
+func (r *JsonnetBinaryRunner) EvaluateFile(filename string) (string, error) {
+	cmd := exec.Command(r.binPath, append(r.args, filename)...)
+
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("error running file %s: %w\n%s", filename, err, string(out))
+	}
+
+	return string(out), nil
+}
+
+// JsonnetBinaryImplementation runs Jsonnet in a subprocess. It doesn't support native functions.
+// The interface of the binary has to compatible with the official Jsonnet CLI.
+// It has to support the following flags:
+// -J <path> for specifying import paths
+// --ext-code <name>=<value> for specifying external variables
+// --tla-code <name>=<value> for specifying top-level arguments
+// --max-stack <value> for specifying the maximum stack size
+// -e <code> for evaluating code snippets
+// <filename> positional arg for evaluating files
+type JsonnetBinaryImplementation struct {
+	BinPath string
+}
+
+func (i *JsonnetBinaryImplementation) MakeEvaluator(importPaths []string, extCode map[string]string, tlaCode map[string]string, maxStack int) types.JsonnetEvaluator {
+	args := []string{}
+	for _, p := range importPaths {
+		args = append(args, "-J", p)
+	}
+	if maxStack > 0 {
+		args = append(args, "--max-stack", strconv.Itoa(maxStack))
+	}
+	for k, v := range extCode {
+		args = append(args, "--ext-code", k+"="+v)
+	}
+	for k, v := range tlaCode {
+		args = append(args, "--tla-code", k+"="+v)
+	}
+
+	return &JsonnetBinaryRunner{
+		binPath: i.BinPath,
+		args:    args,
+	}
+}
diff --git a/pkg/tanka/find.go b/pkg/tanka/find.go
index 43db9575..7e895005 100644
--- a/pkg/tanka/find.go
+++ b/pkg/tanka/find.go
@@ -2,19 +2,23 @@ package tanka
 
 import (
 	"fmt"
-	"os"
 	"path/filepath"
+	"runtime"
+	"time"
 
+	"github.com/grafana/tanka/pkg/jsonnet"
 	"github.com/grafana/tanka/pkg/jsonnet/jpath"
 	"github.com/grafana/tanka/pkg/spec/v1alpha1"
 	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
 	"k8s.io/apimachinery/pkg/labels"
 )
 
 // FindOpts are optional arguments for FindEnvs
 type FindOpts struct {
 	JsonnetOpts
-	Selector labels.Selector
+	Selector    labels.Selector
+	Parallelism int
 }
 
 // FindEnvs returns metadata of all environments recursively found in 'path'.
@@ -22,103 +26,156 @@ type FindOpts struct {
 // static or inline. If a directory is a valid environment, its subdirectories
 // are not checked.
 func FindEnvs(path string, opts FindOpts) ([]*v1alpha1.Environment, error) {
-	// find all environments at dir
-	envs, errs := find(path, Opts{JsonnetOpts: opts.JsonnetOpts})
-	if errs != nil {
-		return envs, ErrParallel{errors: errs}
+	return findEnvsFromPaths([]string{path}, opts)
+}
+
+// FindEnvsFromPaths does the same as FindEnvs but takes a list of paths instead
+func FindEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) {
+	return findEnvsFromPaths(paths, opts)
+}
+
+func findEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) {
+	if opts.Parallelism <= 0 {
+		opts.Parallelism = runtime.NumCPU()
 	}
 
-	// optionally filter
-	if opts.Selector == nil || opts.Selector.Empty() {
-		return envs, nil
+	log.Debug().Int("parallelism", opts.Parallelism).Int("paths", len(paths)).Msg("Finding Tanka environments")
+	startTime := time.Now()
+
+	jsonnetFiles, err := findJsonnetFilesFromPaths(paths, opts)
+	if err != nil {
+		return nil, fmt.Errorf("finding jsonnet files: %w", err)
 	}
 
-	filtered := make([]*v1alpha1.Environment, 0, len(envs))
-	for _, e := range envs {
-		if !opts.Selector.Matches(e.Metadata) {
-			continue
-		}
-		filtered = append(filtered, e)
+	findJsonnetFilesEndTime := time.Now()
+
+	envs, err := findEnvsFromJsonnetFiles(jsonnetFiles, opts)
+	if err != nil {
+		return nil, fmt.Errorf("finding environments: %w", err)
 	}
 
-	return filtered, nil
-}
+	findEnvsEndTime := time.Now()
+
+	log.Info().
+		Int("environments", len(envs)).
+		Dur("ms_to_find_jsonnet_files", findJsonnetFilesEndTime.Sub(startTime)).
+		Dur("ms_to_find_environments", findEnvsEndTime.Sub(findJsonnetFilesEndTime)).
+		Msg("Found Tanka environments")
 
-func findErr(path string, err error) []error {
-	return []error{fmt.Errorf("%s:\n %w", path, err)}
+	return envs, nil
 }
 
-// find implements the actual functionality described at 'FindEnvs'
-func find(path string, opts Opts) ([]*v1alpha1.Environment, []error) {
-	// try if this has envs
-	list, err := List(path, opts)
-	if err != nil &&
-		// expected when looking for environments
-		!errors.As(err, &jpath.ErrorNoBase{}) &&
-		!errors.As(err, &jpath.ErrorFileNotFound{}) {
-		return nil, findErr(path, err)
+// find all jsonnet files within given paths
+func findJsonnetFilesFromPaths(paths []string, opts FindOpts) ([]string, error) {
+	type findJsonnetFilesOut struct {
+		jsonnetFiles []string
+		err          error
 	}
-	if len(list) != 0 {
-		// it has. don't search deeper
-		return list, nil
+
+	pathChan := make(chan string, len(paths))
+	findJsonnetFilesChan := make(chan findJsonnetFilesOut)
+	for i := 0; i < opts.Parallelism; i++ {
+		go func() {
+			for path := range pathChan {
+				jsonnetFiles, err := jsonnet.FindFiles(path, nil)
+				var mainFiles []string
+				for _, file := range jsonnetFiles {
+					if filepath.Base(file) == jpath.DefaultEntrypoint {
+						mainFiles = append(mainFiles, file)
+					}
+				}
+				findJsonnetFilesChan <- findJsonnetFilesOut{jsonnetFiles: mainFiles, err: err}
+			}
+		}()
 	}
 
-	stat, err := os.Stat(path)
-	if err != nil {
-		return nil, findErr(path, err)
+	// push paths to channel
+	for _, path := range paths {
+		pathChan <- path
 	}
+	close(pathChan)
 
-	// if path is a file, don't search deeper
-	if !stat.IsDir() {
-		return nil, nil
+	// collect jsonnet files
+	var jsonnetFiles []string
+	var errs []error
+	for range paths {
+		res := <-findJsonnetFilesChan
+		if res.err != nil {
+			errs = append(errs, res.err)
+			continue
+		}
+		jsonnetFiles = append(jsonnetFiles, res.jsonnetFiles...)
 	}
+	close(findJsonnetFilesChan)
 
-	// list directory
-	files, err := os.ReadDir(path)
-	if err != nil {
-		return nil, findErr(path, err)
+	if len(errs) != 0 {
+		return jsonnetFiles, ErrParallel{errors: errs}
 	}
 
-	// it's not one. Maybe subdirectories are?
-	ch := make(chan findOut)
-	routines := 0
+	return jsonnetFiles, nil
+}
 
-	// recursively find in parallel
-	for _, fi := range files {
-		if !fi.IsDir() {
-			continue
-		}
+// find all environments within jsonnet files
+func findEnvsFromJsonnetFiles(jsonnetFiles []string, opts FindOpts) ([]*v1alpha1.Environment, error) {
+	type findEnvsOut struct {
+		envs []*v1alpha1.Environment
+		err  error
+	}
 
-		routines++
-		go findShim(filepath.Join(path, fi.Name()), opts, ch)
+	jsonnetFilesChan := make(chan string, len(jsonnetFiles))
+	findEnvsChan := make(chan findEnvsOut)
+
+	for i := 0; i < opts.Parallelism; i++ {
+		go func() {
+			for jsonnetFile := range jsonnetFilesChan {
+				// try if this has envs
+				list, err := List(jsonnetFile, Opts{JsonnetOpts: opts.JsonnetOpts})
+				if err != nil &&
+					// expected when looking for environments
+					!errors.As(err, &jpath.ErrorNoBase{}) &&
+					!errors.As(err, &jpath.ErrorFileNotFound{}) {
+					findEnvsChan <- findEnvsOut{err: fmt.Errorf("%s:\n %w", jsonnetFile, err)}
+					continue
+				}
+				filtered := []*v1alpha1.Environment{}
+				// optionally filter
+				if opts.Selector != nil && !opts.Selector.Empty() {
+					for _, e := range list {
+						if !opts.Selector.Matches(e.Metadata) {
+							continue
+						}
+						filtered = append(filtered, e)
+					}
+				} else {
+					filtered = append(filtered, list...)
+				}
+				findEnvsChan <- findEnvsOut{envs: filtered, err: nil}
+			}
+		}()
 	}
 
-	// collect parallel results
-	var errs []error
-	var envs []*v1alpha1.Environment
+	// push jsonnet files to channel
+	for _, jsonnetFile := range jsonnetFiles {
+		jsonnetFilesChan <- jsonnetFile
+	}
+	close(jsonnetFilesChan)
 
-	for i := 0; i < routines; i++ {
-		out := <-ch
-		if out.errs != nil {
-			errs = append(errs, out.errs...)
+	// collect environments
+	var envs []*v1alpha1.Environment
+	var errs []error
+	for i := 0; i < len(jsonnetFiles); i++ {
+		res := <-findEnvsChan
+		if res.err != nil {
+			errs = append(errs, res.err)
+			continue
 		}
-
-		envs = append(envs, out.envs...)
+		envs = append(envs, res.envs...)
 	}
+	close(findEnvsChan)
 
 	if len(errs) != 0 {
-		return envs, errs
+		return envs, ErrParallel{errors: errs}
 	}
 
 	return envs, nil
 }
-
-type findOut struct {
-	envs []*v1alpha1.Environment
-	errs []error
-}
-
-func findShim(dir string, opts Opts, ch chan findOut) {
-	envs, errs := find(dir, opts)
-	ch <- findOut{envs: envs, errs: errs}
-}
diff --git a/pkg/tanka/load.go b/pkg/tanka/load.go
index 5e30efd3..0721c6f7 100644
--- a/pkg/tanka/load.go
+++ b/pkg/tanka/load.go
@@ -4,7 +4,9 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 
+	"github.com/grafana/tanka/pkg/jsonnet/implementations/binary"
 	"github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl"
 	"github.com/grafana/tanka/pkg/jsonnet/implementations/types"
 	"github.com/grafana/tanka/pkg/jsonnet/jpath"
@@ -103,6 +105,23 @@ func List(path string, opts Opts) ([]*v1alpha1.Environment, error) {
 }
 
 func getJsonnetImplementation(path string, opts Opts) (types.JsonnetImplementation, error) {
+	if strings.HasPrefix(opts.JsonnetImplementation, "binary:") {
+		binPath := strings.TrimPrefix(opts.JsonnetImplementation, "binary:")
+
+		// check if binary exists and is executable
+		stat, err := os.Stat(binPath)
+		if err != nil {
+			return nil, fmt.Errorf("binary %q does not exist", binPath)
+		}
+		if stat.Mode()&0111 == 0 {
+			return nil, fmt.Errorf("binary %q is not executable", binPath)
+		}
+
+		return &binary.JsonnetBinaryImplementation{
+			BinPath: binPath,
+		}, nil
+	}
+
 	switch opts.JsonnetImplementation {
 	case "go", "":
 		return &goimpl.JsonnetGoImplementation{
diff --git a/pkg/tanka/parallel.go b/pkg/tanka/parallel.go
index 5b4268b1..483e0739 100644
--- a/pkg/tanka/parallel.go
+++ b/pkg/tanka/parallel.go
@@ -42,7 +42,11 @@ func parallelLoadEnvironments(envs []*v1alpha1.Environment, opts parallelOpts) (
 	for _, env := range envs {
 		o := opts.Opts
 
-		if o.JsonnetImplementation == "" {
+		if env.Spec.ExportJsonnetImplementation != "" {
+			log.Trace().
+				Str("name", env.Metadata.Name).
+				Str("implementation", env.Spec.ExportJsonnetImplementation).
+				Msg("Using custom Jsonnet implementation")
 			o.JsonnetImplementation = env.Spec.ExportJsonnetImplementation
 		}
 


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..7e10bbe0
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,42 @@
+name: Tests
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - '*'
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v4
+        with:
+          go-version: '1.20'
+      - run: make lint
+  
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v4
+        with:
+          go-version: '1.20'
+      - uses: azure/setup-helm@v3
+        with:
+          version: '3.13.1' 
+      - name: Install jsonnet
+        run: go install github.com/google/go-jsonnet/cmd/jsonnet@v0.20.0
+      - run: make test
+
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v4
+        with:
+          go-version: '1.20'
+      - run: make cross
diff --git a/pkg/jsonnet/eval_test.go b/pkg/jsonnet/eval_test.go
index db15b3de..a1051502 100644
--- a/pkg/jsonnet/eval_test.go
+++ b/pkg/jsonnet/eval_test.go
@@ -2,9 +2,11 @@ package jsonnet
 
 import (
 	"os"
+	"os/exec"
 	"path/filepath"
 	"testing"
 
+	"github.com/grafana/tanka/pkg/jsonnet/implementations/binary"
 	"github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -59,6 +61,21 @@ func TestEvaluateFile(t *testing.T) {
 	assert.Equal(t, thisFileResult, result)
 }
 
+func TestEvaluateFileWithInvalidBinary(t *testing.T) {
+	binaryImpl := &binary.JsonnetBinaryImplementation{BinPath: "this-file-doesnt-exist"}
+	result, err := EvaluateFile(binaryImpl, "testdata/thisFile/main.jsonnet", Opts{})
+	assert.Equal(t, result, "")
+	assert.ErrorIs(t, err, exec.ErrNotFound)
+}
+
+// This test requires jsonnet to be installed and available in the PATH
+func TestEvaluateFileWithJsonnetBinary(t *testing.T) {
+	binaryImpl := &binary.JsonnetBinaryImplementation{BinPath: "jsonnet"}
+	result, err := EvaluateFile(binaryImpl, "testdata/thisFile/main.jsonnet", Opts{})
+	assert.NoError(t, err)
+	assert.Equal(t, thisFileResult, result)
+}
+
 func TestEvaluateFileDoesntExist(t *testing.T) {
 	result, err := EvaluateFile(jsonnetImpl, "testdata/doesnt-exist/main.jsonnet", Opts{})
 	assert.EqualError(t, err, "open testdata/doesnt-exist/main.jsonnet: no such file or directory")
diff --git a/pkg/tanka/find_test.go b/pkg/tanka/find_test.go
new file mode 100644
index 00000000..5f2f20a0
--- /dev/null
+++ b/pkg/tanka/find_test.go
@@ -0,0 +1,98 @@
+package tanka
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func BenchmarkFindEnvsFromSinglePath(b *testing.B) {
+	tempDir, envPaths := buildLargeEnvironmentDirForFindTest(b)
+	require.Len(b, envPaths, 105) // 100 static + 5 inline
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		envs, err := FindEnvs(tempDir, FindOpts{})
+		require.Len(b, envs, 200)
+		require.NoError(b, err)
+	}
+}
+
+func BenchmarkFindEnvsFromPaths(b *testing.B) {
+	_, envPaths := buildLargeEnvironmentDirForFindTest(b)
+	require.Len(b, envPaths, 105) // 100 static + 5 inline
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		envs, err := FindEnvsFromPaths(envPaths, FindOpts{})
+		require.Len(b, envs, 200)
+		require.NoError(b, err)
+	}
+}
+
+// create a directory with lots of inline and static environments
+func buildLargeEnvironmentDirForFindTest(t testing.TB) (string, []string) {
+	t.Helper()
+
+	// create a temp dir
+	tempDir := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(tempDir, "jsonnetfile.json"), []byte(`{}`), 0644))
+	var envPaths []string
+
+	// create 100 static envs: 5 indent levels (dir-0, dir-0/dir-1, etc) + 20 static envs each)
+	// create 100 inline envs: 5 indent levels (dir-0, dir-0/dir-1, etc) + 1 inline env dir in each with 20 envs each)
+	for indent := range [5]struct{}{} {
+		// create indented dir
+		envDir := tempDir
+		for i := 0; i < indent; i++ {
+			envDir = filepath.Join(envDir, fmt.Sprintf("dir-%d", indent))
+		}
+
+		// create 20 static envs
+		for id := range [20]struct{}{} {
+			staticEnvDir := filepath.Join(envDir, fmt.Sprintf("static-%d", id))
+			staticEnvFile := filepath.Join(staticEnvDir, "main.jsonnet")
+			envPaths = append(envPaths, staticEnvDir)
+			require.NoError(t, os.MkdirAll(staticEnvDir, 0755))
+			require.NoError(t, os.WriteFile(staticEnvFile, []byte(`{}`), 0644))
+			require.NoError(t, os.WriteFile(filepath.Join(staticEnvDir, "spec.json"), []byte(fmt.Sprintf(`{
+    "apiVersion": "tanka.dev/v1alpha1",
+    "kind": "Environment",
+    "metadata": {
+		"name": "%[1]s",
+		"labels": {}
+    },
+    "spec": {
+		"apiServer": "https://192.168.0.1",
+		"namespace": "blabla",
+		"cluster": "blabla"
+	}
+}`, staticEnvDir)), 0644))
+		}
+
+		inlineEnvDir := filepath.Join(envDir, "inline")
+		inlineEnvFile := filepath.Join(inlineEnvDir, "main.jsonnet")
+		envPaths = append(envPaths, inlineEnvDir)
+		require.NoError(t, os.MkdirAll(inlineEnvDir, 0755))
+		require.NoError(t, os.WriteFile(inlineEnvFile, []byte(fmt.Sprintf(`[
+{
+	"apiVersion": "tanka.dev/v1alpha1",
+	"kind": "Environment",
+	"metadata": {
+		"name": "%[1]s/%%s",
+		"labels": {}
+	},
+	"spec": {
+		"apiServer": "https://192.168.0.1",
+		"namespace": "blabla",
+		"cluster": "blabla",
+	}
+} for i in std.range(0, 19)
+		]`, inlineEnvDir)), 0644))
+	}
+
+	return tempDir, envPaths
+}
diff --git a/pkg/tanka/load_test.go b/pkg/tanka/load_test.go
index f82774b2..b2f44d6f 100644
--- a/pkg/tanka/load_test.go
+++ b/pkg/tanka/load_test.go
@@ -2,10 +2,15 @@ package tanka
 
 import (
 	"os"
+	"path/filepath"
 	"testing"
 
+	"github.com/grafana/tanka/pkg/jsonnet/implementations/binary"
+	"github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl"
+	"github.com/grafana/tanka/pkg/jsonnet/implementations/types"
 	"github.com/grafana/tanka/pkg/kubernetes/manifest"
 	"github.com/grafana/tanka/pkg/spec/v1alpha1"
+	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -211,3 +216,68 @@ func TestLoadFailsWhenBothSpecAndInline(t *testing.T) {
 	_, err := Load("./testdata/cases/static-and-inline", Opts{Name: "inline"})
 	assert.EqualError(t, err, "found a tanka Environment resource. Check that you aren't using a spec.json and inline environments simultaneously")
 }
+
+func TestGetJsonnetImplementation(t *testing.T) {
+	tempDir := t.TempDir()
+
+	// Write file that is not executable
+	notExecutablePath := filepath.Join(tempDir, "not-executable")
+	require.NoError(t, os.WriteFile(notExecutablePath, []byte("not executable"), 0644))
+
+	// Write file that is executable
+	executablePath := filepath.Join(tempDir, "executable")
+	require.NoError(t, os.WriteFile(executablePath, []byte("executable"), 0755))
+
+	cases := []struct {
+		implementationName string
+		path               string
+		expected           types.JsonnetImplementation
+		expectedErr        error
+	}{
+		{
+			implementationName: "",
+			path:               "my-dir",
+			expected: &goimpl.JsonnetGoImplementation{
+				Path: "my-dir",
+			},
+		},
+		{
+			implementationName: "go",
+			path:               "my-dir",
+			expected: &goimpl.JsonnetGoImplementation{
+				Path: "my-dir",
+			},
+		},
+		{
+			implementationName: "binary:does-not-exist",
+			expectedErr:        errors.New(`binary "does-not-exist" does not exist`),
+		},
+		{
+			implementationName: "binary:" + notExecutablePath,
+			expectedErr:        errors.New(`binary "` + notExecutablePath + `" is not executable`),
+		},
+		{
+			implementationName: "binary:" + executablePath,
+			expected: &binary.JsonnetBinaryImplementation{
+				BinPath: executablePath,
+			},
+		},
+		{
+			implementationName: "invalid",
+			expectedErr:        errors.New("unknown jsonnet implementation: invalid"),
+		},
+	}
+
+	for _, tt := range cases {
+		t.Run(tt.implementationName, func(t *testing.T) {
+			implementation, err := getJsonnetImplementation(tt.path, Opts{JsonnetImplementation: tt.implementationName})
+
+			if tt.expectedErr != nil {
+				assert.EqualError(t, err, tt.expectedErr.Error())
+				return
+			}
+
+			assert.Equal(t, tt.expected, implementation)
+		})
+	}
+}