feat(images): preinstall Go and Node.js in the tool cache of the job image
- commit
- be87239e26b6225ea1960ec349d7639826e26b48
- author
- brice · · 2026-10-21 05:29 UTC
- parent
- e52d13c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +143 -2
M docs/images.md +16 -2
| @@ -7,7 +7,7 @@ A job runs in the image named by `ERMITE_RUNNER_IMAGE`. | |||
| 7 | 7 | | `container` | OCI reference | Default `ermite-runner-ubuntu-26.04:latest` | | |
| 8 | 8 | | `vm` | Directory | `vmlinux` (`Image` on arm64) and `base.ext4`. Default `<state dir>/image` | | |
| 9 | 9 | ||
| 10 | - | Both come from `images/ubuntu-26.04/Dockerfile` (Ubuntu 26.04 LTS). The runner executable and Node.js are attached by the host. They are never part of the image. | |
| 10 | + | Both come from `images/ubuntu-26.04/Dockerfile` (Ubuntu 26.04 LTS). The runner executable and action runtime Node.js are attached by the host. The image also includes system Node.js and the cached toolchains listed below. | |
| 11 | 11 | ||
| 12 | 12 | ## Build | |
| 13 | 13 | ||
| @@ -26,9 +26,23 @@ scripts/build-image.sh tmp/image # builds the OCI image and tmp/image/base.ex | |||
| 26 | 26 | ## What the image must contain | |
| 27 | 27 | ||
| 28 | 28 | - User `runner`, uid and gid 1001, home `/home/runner`. Steps run as this user. Without it, steps run as root (test images only). | |
| 29 | - | - `/home/runner/work` and `/opt/hostedtoolcache`, owned by `runner`. | |
| 29 | + | - `/home/runner/work` and `/opt/hostedtoolcache`, owned by `runner`. The tool cache holds the toolchains listed in `images/ubuntu-26.04/toolcache.lock` for the image architecture. | |
| 30 | 30 | - `bash` and `sh`. `python3` and a `python` command for `shell: python`. | |
| 31 | 31 | - `dockerd` and passwordless `sudo` for `runner`, for jobs that use Docker. | |
| 32 | 32 | - Common tools: git, curl, jq, build-essential, ca-certificates, tzdata, zip and unzip. | |
| 33 | 33 | ||
| 34 | 34 | `pwsh` is not in the image. A step that asks for it fails with `pwsh: command not found`. | |
| 35 | + | ||
| 36 | + | ## Toolchains | |
| 37 | + | ||
| 38 | + | `images/ubuntu-26.04/toolcache.lock` pins Go and Node.js archives by version, | |
| 39 | + | action architecture (`x64` or `arm64`), official SHA-256 and download URL. | |
| 40 | + | To add a version, add a line for each supported architecture with the checksum | |
| 41 | + | from go.dev's download JSON or nodejs.org's `SHASUMS256.txt`, then rebuild with | |
| 42 | + | `scripts/build-image.sh tmp/image`. A checksum mismatch fails the build. | |
| 43 | + | ||
| 44 | + | The installer uses the `@actions/tool-cache` layout, including the sibling | |
| 45 | + | `<arch>.complete` marker. `actions/setup-go` with an exact version, including | |
| 46 | + | one read from `go.mod`, uses the cache only for an exact match. A range such as | |
| 47 | + | `1.27` selects the highest matching cached version. Versions not in the cache | |
| 48 | + | still download normally (and `check-latest: true` may check upstream). | |
| @@ -7,7 +7,7 @@ A job runs in the image named by `ERMITE_RUNNER_IMAGE`. | |||||
| 7 | | `container` | OCI reference | Default `ermite-runner-ubuntu-26.04:latest` | | 7 | | `container` | OCI reference | Default `ermite-runner-ubuntu-26.04:latest` | | ||
| 8 | | `vm` | Directory | `vmlinux` (`Image` on arm64) and `base.ext4`. Default `<state dir>/image` | | 8 | | `vm` | Directory | `vmlinux` (`Image` on arm64) and `base.ext4`. Default `<state dir>/image` | | ||
| 9 | 9 | ||||
| 10 | - | Both come from `images/ubuntu-26.04/Dockerfile` (Ubuntu 26.04 LTS). The runner executable and Node.js are attached by the host. They are never part of the image. | 10 | + | Both come from `images/ubuntu-26.04/Dockerfile` (Ubuntu 26.04 LTS). The runner executable and action runtime Node.js are attached by the host. The image also includes system Node.js and the cached toolchains listed below. |
| 11 | 11 | ||||
| 12 | ## Build | 12 | ## Build | ||
| 13 | 13 | ||||
| @@ -26,9 +26,23 @@ scripts/build-image.sh tmp/image # builds the OCI image and tmp/image/base.ex | |||||
| 26 | ## What the image must contain | 26 | ## What the image must contain | ||
| 27 | 27 | ||||
| 28 | - User `runner`, uid and gid 1001, home `/home/runner`. Steps run as this user. Without it, steps run as root (test images only). | 28 | - User `runner`, uid and gid 1001, home `/home/runner`. Steps run as this user. Without it, steps run as root (test images only). | ||
| 29 | - | - `/home/runner/work` and `/opt/hostedtoolcache`, owned by `runner`. | 29 | + | - `/home/runner/work` and `/opt/hostedtoolcache`, owned by `runner`. The tool cache holds the toolchains listed in `images/ubuntu-26.04/toolcache.lock` for the image architecture. |
| 30 | - `bash` and `sh`. `python3` and a `python` command for `shell: python`. | 30 | - `bash` and `sh`. `python3` and a `python` command for `shell: python`. | ||
| 31 | - `dockerd` and passwordless `sudo` for `runner`, for jobs that use Docker. | 31 | - `dockerd` and passwordless `sudo` for `runner`, for jobs that use Docker. | ||
| 32 | - Common tools: git, curl, jq, build-essential, ca-certificates, tzdata, zip and unzip. | 32 | - Common tools: git, curl, jq, build-essential, ca-certificates, tzdata, zip and unzip. | ||
| 33 | 33 | ||||
| 34 | `pwsh` is not in the image. A step that asks for it fails with `pwsh: command not found`. | 34 | `pwsh` is not in the image. A step that asks for it fails with `pwsh: command not found`. | ||
| 35 | + | ||||
| 36 | + | ## Toolchains | |||
| 37 | + | ||||
| 38 | + | `images/ubuntu-26.04/toolcache.lock` pins Go and Node.js archives by version, | |||
| 39 | + | action architecture (`x64` or `arm64`), official SHA-256 and download URL. | |||
| 40 | + | To add a version, add a line for each supported architecture with the checksum | |||
| 41 | + | from go.dev's download JSON or nodejs.org's `SHASUMS256.txt`, then rebuild with | |||
| 42 | + | `scripts/build-image.sh tmp/image`. A checksum mismatch fails the build. | |||
| 43 | + | ||||
| 44 | + | The installer uses the `@actions/tool-cache` layout, including the sibling | |||
| 45 | + | `<arch>.complete` marker. `actions/setup-go` with an exact version, including | |||
| 46 | + | one read from `go.mod`, uses the cache only for an exact match. A range such as | |||
| 47 | + | `1.27` selects the highest matching cached version. Versions not in the cache | |||
| 48 | + | still download normally (and `check-latest: true` may check upstream). | |||
M images/ubuntu-26.04/Dockerfile +4 -0
| @@ -78,6 +78,10 @@ RUN useradd --create-home --uid 1001 --shell /bin/bash runner \ | |||
| 78 | 78 | ENV RUNNER_TOOL_CACHE=/opt/hostedtoolcache \ | |
| 79 | 79 | LANG=C.UTF-8 | |
| 80 | 80 | ||
| 81 | + | COPY toolcache.lock install-toolcache.sh /tmp/toolcache/ | |
| 82 | + | RUN sh /tmp/toolcache/install-toolcache.sh /tmp/toolcache/toolcache.lock \ | |
| 83 | + | && rm -rf /tmp/toolcache | |
| 84 | + | ||
| 81 | 85 | # The runner executable is bind-mounted (container) or provided by the init | |
| 82 | 86 | # disk (vm) here; its init subcommand starts the job's own Docker daemon. | |
| 83 | 87 | ENTRYPOINT ["/opt/ermite/ermite-runner", "init"] | |
| @@ -78,6 +78,10 @@ RUN useradd --create-home --uid 1001 --shell /bin/bash runner \ | |||||
| 78 | ENV RUNNER_TOOL_CACHE=/opt/hostedtoolcache \ | 78 | ENV RUNNER_TOOL_CACHE=/opt/hostedtoolcache \ | ||
| 79 | LANG=C.UTF-8 | 79 | LANG=C.UTF-8 | ||
| 80 | 80 | ||||
| 81 | + | COPY toolcache.lock install-toolcache.sh /tmp/toolcache/ | |||
| 82 | + | RUN sh /tmp/toolcache/install-toolcache.sh /tmp/toolcache/toolcache.lock \ | |||
| 83 | + | && rm -rf /tmp/toolcache | |||
| 84 | + | ||||
| 81 | # The runner executable is bind-mounted (container) or provided by the init | 85 | # The runner executable is bind-mounted (container) or provided by the init | ||
| 82 | # disk (vm) here; its init subcommand starts the job's own Docker daemon. | 86 | # disk (vm) here; its init subcommand starts the job's own Docker daemon. | ||
| 83 | ENTRYPOINT ["/opt/ermite/ermite-runner", "init"] | 87 | ENTRYPOINT ["/opt/ermite/ermite-runner", "init"] | ||
A images/ubuntu-26.04/install-toolcache.sh +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # SPDX-License-Identifier: MIT | |
| 3 | + | set -eu | |
| 4 | + | ||
| 5 | + | case "$(dpkg --print-architecture)" in | |
| 6 | + | amd64) arch=x64 ;; | |
| 7 | + | arm64) arch=arm64 ;; | |
| 8 | + | *) echo 'unsupported toolcache architecture' >&2; exit 1 ;; | |
| 9 | + | esac | |
| 10 | + | cache=${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache} | |
| 11 | + | tmp=$(mktemp -d) | |
| 12 | + | trap 'rm -rf "$tmp"' EXIT HUP INT TERM | |
| 13 | + | while read -r tool version entry_arch sha url; do | |
| 14 | + | case "$tool" in ''|'#'*) continue ;; esac | |
| 15 | + | [ "$entry_arch" = "$arch" ] || continue | |
| 16 | + | case "$tool" in go|node) ;; *) echo "unsupported tool: $tool" >&2; exit 1 ;; esac | |
| 17 | + | case "$url" in | |
| 18 | + | *.tar.gz) compression=z ;; | |
| 19 | + | *.tar.xz) compression=J ;; | |
| 20 | + | *) echo "unsupported archive: $url" >&2; exit 1 ;; | |
| 21 | + | esac | |
| 22 | + | curl -fsSL "$url" -o "$tmp/archive" | |
| 23 | + | printf '%s %s\n' "$sha" "$tmp/archive" | sha256sum -c - | |
| 24 | + | dest="$cache/$tool/$version/$arch" | |
| 25 | + | mkdir -p "$dest" | |
| 26 | + | tar -x"$compression"f "$tmp/archive" -C "$dest" --strip-components=1 --no-same-owner | |
| 27 | + | touch "$dest.complete" | |
| 28 | + | rm "$tmp/archive" | |
| 29 | + | done < "${1:?usage: install-toolcache.sh LOCK_FILE}" | |
| 30 | + | chown -R runner:runner "$cache" | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | #!/bin/sh | |||
| 2 | + | # SPDX-License-Identifier: MIT | |||
| 3 | + | set -eu | |||
| 4 | + | ||||
| 5 | + | case "$(dpkg --print-architecture)" in | |||
| 6 | + | amd64) arch=x64 ;; | |||
| 7 | + | arm64) arch=arm64 ;; | |||
| 8 | + | *) echo 'unsupported toolcache architecture' >&2; exit 1 ;; | |||
| 9 | + | esac | |||
| 10 | + | cache=${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache} | |||
| 11 | + | tmp=$(mktemp -d) | |||
| 12 | + | trap 'rm -rf "$tmp"' EXIT HUP INT TERM | |||
| 13 | + | while read -r tool version entry_arch sha url; do | |||
| 14 | + | case "$tool" in ''|'#'*) continue ;; esac | |||
| 15 | + | [ "$entry_arch" = "$arch" ] || continue | |||
| 16 | + | case "$tool" in go|node) ;; *) echo "unsupported tool: $tool" >&2; exit 1 ;; esac | |||
| 17 | + | case "$url" in | |||
| 18 | + | *.tar.gz) compression=z ;; | |||
| 19 | + | *.tar.xz) compression=J ;; | |||
| 20 | + | *) echo "unsupported archive: $url" >&2; exit 1 ;; | |||
| 21 | + | esac | |||
| 22 | + | curl -fsSL "$url" -o "$tmp/archive" | |||
| 23 | + | printf '%s %s\n' "$sha" "$tmp/archive" | sha256sum -c - | |||
| 24 | + | dest="$cache/$tool/$version/$arch" | |||
| 25 | + | mkdir -p "$dest" | |||
| 26 | + | tar -x"$compression"f "$tmp/archive" -C "$dest" --strip-components=1 --no-same-owner | |||
| 27 | + | touch "$dest.complete" | |||
| 28 | + | rm "$tmp/archive" | |||
| 29 | + | done < "${1:?usage: install-toolcache.sh LOCK_FILE}" | |||
| 30 | + | chown -R runner:runner "$cache" | |||
A images/ubuntu-26.04/toolcache.lock +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | # tool version arch sha256 url | |
| 2 | + | go 1.27.3 x64 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40 https://go.dev/dl/go1.27.3.linux-amd64.tar.gz | |
| 3 | + | go 1.27.3 arm64 a06e99309dbcbdca1a8ccd2282c3ee146e55930886e26b4f1193269587165f49 https://go.dev/dl/go1.27.3.linux-arm64.tar.gz | |
| 4 | + | go 1.27.1 x64 63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445 https://go.dev/dl/go1.27.1.linux-amd64.tar.gz | |
| 5 | + | go 1.27.1 arm64 3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec https://go.dev/dl/go1.27.1.linux-arm64.tar.gz | |
| 6 | + | go 1.26.10 x64 ccfe7ae822d2c2f41f4fe7c956199967a4dac22d0ee0d2b2bfdb440aab3b20c5 https://go.dev/dl/go1.26.10.linux-amd64.tar.gz | |
| 7 | + | go 1.26.10 arm64 3f4227ce7b206a513e794f1393adb473a07b46e94d18bd15d1c684bbb7319797 https://go.dev/dl/go1.26.10.linux-arm64.tar.gz | |
| 8 | + | node 24.21.0 x64 fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6 https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.xz | |
| 9 | + | node 24.21.0 arm64 6ad1325edbdb5649c379b75a237147a666c95d4f9ae8d340fef2d1575d289ad2 https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.xz | |
| 10 | + | node 22.23.3 x64 df450af89261115ef9f9e3830c3eeb2cc9213b63c720b1af623cb5dcbe2e02de https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-x64.tar.xz | |
| 11 | + | node 22.23.3 arm64 a44aeb94849a299b22df10b9e622ec2f605c2183501bc40590705131de7c740f https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-arm64.tar.xz | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | # tool version arch sha256 url | |||
| 2 | + | go 1.27.3 x64 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40 https://go.dev/dl/go1.27.3.linux-amd64.tar.gz | |||
| 3 | + | go 1.27.3 arm64 a06e99309dbcbdca1a8ccd2282c3ee146e55930886e26b4f1193269587165f49 https://go.dev/dl/go1.27.3.linux-arm64.tar.gz | |||
| 4 | + | go 1.27.1 x64 63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445 https://go.dev/dl/go1.27.1.linux-amd64.tar.gz | |||
| 5 | + | go 1.27.1 arm64 3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec https://go.dev/dl/go1.27.1.linux-arm64.tar.gz | |||
| 6 | + | go 1.26.10 x64 ccfe7ae822d2c2f41f4fe7c956199967a4dac22d0ee0d2b2bfdb440aab3b20c5 https://go.dev/dl/go1.26.10.linux-amd64.tar.gz | |||
| 7 | + | go 1.26.10 arm64 3f4227ce7b206a513e794f1393adb473a07b46e94d18bd15d1c684bbb7319797 https://go.dev/dl/go1.26.10.linux-arm64.tar.gz | |||
| 8 | + | node 24.21.0 x64 fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6 https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.xz | |||
| 9 | + | node 24.21.0 arm64 6ad1325edbdb5649c379b75a237147a666c95d4f9ae8d340fef2d1575d289ad2 https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.xz | |||
| 10 | + | node 22.23.3 x64 df450af89261115ef9f9e3830c3eeb2cc9213b63c720b1af623cb5dcbe2e02de https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-x64.tar.xz | |||
| 11 | + | node 22.23.3 arm64 a44aeb94849a299b22df10b9e622ec2f605c2183501bc40590705131de7c740f https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-arm64.tar.xz | |||
A internal/e2e/toolcache_test.go +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package e2e | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "os/exec" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite-runner/internal/execstream" | |
| 13 | + | "bnema.dev/ermite-runner/internal/execstream/execpb" | |
| 14 | + | "bnema.dev/ermite-runner/internal/sandbox" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | func TestContainerSetupGoFindsToolcache(t *testing.T) { | |
| 18 | + | image := needE2E(t) | |
| 19 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) | |
| 20 | + | defer cancel() | |
| 21 | + | // Distinguish a missing cache from an unavailable image or Docker daemon. | |
| 22 | + | out, err := exec.CommandContext(ctx, "docker", "run", "--rm", "--entrypoint", "sh", image, "-c", "if test -d /opt/hostedtoolcache/go/1.27.1; then echo present; else echo absent; fi").CombinedOutput() | |
| 23 | + | if err != nil { | |
| 24 | + | t.Fatalf("probe toolcache: %v\n%s", err, out) | |
| 25 | + | } | |
| 26 | + | if strings.TrimSpace(string(out)) == "absent" { | |
| 27 | + | t.Skip("test image has no Go 1.27.1 toolcache") | |
| 28 | + | } | |
| 29 | + | d := newDriver(t, image) | |
| 30 | + | sb, err := d.New() | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | defer func() { | |
| 35 | + | if err := sb.Stop(context.Background()); err != nil { | |
| 36 | + | t.Error(err) | |
| 37 | + | } | |
| 38 | + | }() | |
| 39 | + | conn, err := sb.Start(ctx, sandbox.JobSpec{JobID: "e2e-toolcache"}) | |
| 40 | + | if err != nil { | |
| 41 | + | t.Fatal(err) | |
| 42 | + | } | |
| 43 | + | spec := jobSpec(`on: push | |
| 44 | + | jobs: | |
| 45 | + | j: | |
| 46 | + | runs-on: x | |
| 47 | + | steps: | |
| 48 | + | - uses: actions/setup-go@v7 | |
| 49 | + | with: | |
| 50 | + | go-version: '1.27.1' | |
| 51 | + | cache: false | |
| 52 | + | - run: go version | |
| 53 | + | `) | |
| 54 | + | var logs []string | |
| 55 | + | var result *execpb.JobResult | |
| 56 | + | _, exit, err := execstream.DriveJob(conn, &execpb.Run{Job: spec}, nil, execstream.JobHandler{ | |
| 57 | + | Resolve: func(_ context.Context, r *execpb.ResolveRequest) *execpb.ResolveResponse { | |
| 58 | + | resp := &execpb.ResolveResponse{} | |
| 59 | + | for _, ref := range r.Uses { | |
| 60 | + | if ref != "actions/setup-go@v7" { | |
| 61 | + | return &execpb.ResolveResponse{Error: "unexpected action: " + ref} | |
| 62 | + | } | |
| 63 | + | resp.Actions = append(resp.Actions, &execpb.ResolvedAction{Ref: ref, NameWithOwner: "actions/setup-go", ResolvedSha: "v7", TarballUrl: "https://codeload.github.com/actions/setup-go/tar.gz/refs/tags/v7"}) | |
| 64 | + | } | |
| 65 | + | return resp | |
| 66 | + | }, | |
| 67 | + | Log: func(l *execpb.Log) { logs = append(logs, l.Text) }, | |
| 68 | + | JobResult: func(r *execpb.JobResult) { result = r }, | |
| 69 | + | }) | |
| 70 | + | all := strings.Join(logs, "\n") | |
| 71 | + | if err != nil { | |
| 72 | + | t.Fatalf("drive: %v\n%s", err, all) | |
| 73 | + | } | |
| 74 | + | if result == nil || result.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS || exit == nil || exit.Code != 0 { | |
| 75 | + | t.Fatalf("result %v exit %v\n%s", result, exit, all) | |
| 76 | + | } | |
| 77 | + | for _, want := range []string{"Found in cache", "go version go1.27.1"} { | |
| 78 | + | if !strings.Contains(all, want) { | |
| 79 | + | t.Errorf("log lacks %q\n%s", want, all) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| @@ -0,0 +1,82 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package e2e | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "os/exec" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/ermite-runner/internal/execstream" | |||
| 13 | + | "bnema.dev/ermite-runner/internal/execstream/execpb" | |||
| 14 | + | "bnema.dev/ermite-runner/internal/sandbox" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | func TestContainerSetupGoFindsToolcache(t *testing.T) { | |||
| 18 | + | image := needE2E(t) | |||
| 19 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) | |||
| 20 | + | defer cancel() | |||
| 21 | + | // Distinguish a missing cache from an unavailable image or Docker daemon. | |||
| 22 | + | out, err := exec.CommandContext(ctx, "docker", "run", "--rm", "--entrypoint", "sh", image, "-c", "if test -d /opt/hostedtoolcache/go/1.27.1; then echo present; else echo absent; fi").CombinedOutput() | |||
| 23 | + | if err != nil { | |||
| 24 | + | t.Fatalf("probe toolcache: %v\n%s", err, out) | |||
| 25 | + | } | |||
| 26 | + | if strings.TrimSpace(string(out)) == "absent" { | |||
| 27 | + | t.Skip("test image has no Go 1.27.1 toolcache") | |||
| 28 | + | } | |||
| 29 | + | d := newDriver(t, image) | |||
| 30 | + | sb, err := d.New() | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | defer func() { | |||
| 35 | + | if err := sb.Stop(context.Background()); err != nil { | |||
| 36 | + | t.Error(err) | |||
| 37 | + | } | |||
| 38 | + | }() | |||
| 39 | + | conn, err := sb.Start(ctx, sandbox.JobSpec{JobID: "e2e-toolcache"}) | |||
| 40 | + | if err != nil { | |||
| 41 | + | t.Fatal(err) | |||
| 42 | + | } | |||
| 43 | + | spec := jobSpec(`on: push | |||
| 44 | + | jobs: | |||
| 45 | + | j: | |||
| 46 | + | runs-on: x | |||
| 47 | + | steps: | |||
| 48 | + | - uses: actions/setup-go@v7 | |||
| 49 | + | with: | |||
| 50 | + | go-version: '1.27.1' | |||
| 51 | + | cache: false | |||
| 52 | + | - run: go version | |||
| 53 | + | `) | |||
| 54 | + | var logs []string | |||
| 55 | + | var result *execpb.JobResult | |||
| 56 | + | _, exit, err := execstream.DriveJob(conn, &execpb.Run{Job: spec}, nil, execstream.JobHandler{ | |||
| 57 | + | Resolve: func(_ context.Context, r *execpb.ResolveRequest) *execpb.ResolveResponse { | |||
| 58 | + | resp := &execpb.ResolveResponse{} | |||
| 59 | + | for _, ref := range r.Uses { | |||
| 60 | + | if ref != "actions/setup-go@v7" { | |||
| 61 | + | return &execpb.ResolveResponse{Error: "unexpected action: " + ref} | |||
| 62 | + | } | |||
| 63 | + | resp.Actions = append(resp.Actions, &execpb.ResolvedAction{Ref: ref, NameWithOwner: "actions/setup-go", ResolvedSha: "v7", TarballUrl: "https://codeload.github.com/actions/setup-go/tar.gz/refs/tags/v7"}) | |||
| 64 | + | } | |||
| 65 | + | return resp | |||
| 66 | + | }, | |||
| 67 | + | Log: func(l *execpb.Log) { logs = append(logs, l.Text) }, | |||
| 68 | + | JobResult: func(r *execpb.JobResult) { result = r }, | |||
| 69 | + | }) | |||
| 70 | + | all := strings.Join(logs, "\n") | |||
| 71 | + | if err != nil { | |||
| 72 | + | t.Fatalf("drive: %v\n%s", err, all) | |||
| 73 | + | } | |||
| 74 | + | if result == nil || result.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS || exit == nil || exit.Code != 0 { | |||
| 75 | + | t.Fatalf("result %v exit %v\n%s", result, exit, all) | |||
| 76 | + | } | |||
| 77 | + | for _, want := range []string{"Found in cache", "go version go1.27.1"} { | |||
| 78 | + | if !strings.Contains(all, want) { | |||
| 79 | + | t.Errorf("log lacks %q\n%s", want, all) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||