From 60cf0b02ae4cfc237af33b0a7973af99d30f2cd7 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 26 Sep 2026 10:15:28 +0100 Subject: [PATCH 1/2] Support supplied HTTP clients under TinyGo --- .github/workflows/ci.yml | 9 +++ README.md | 2 + client/transport_portable_test.go | 46 +++++++++++++++ client/transport_tinygo_test.go | 32 +++++++++++ fetch/fetcher.go | 96 ++++--------------------------- fetch/transport_portable_test.go | 88 ++++++++++++++++++++++++++++ fetch/transport_std.go | 88 ++++++++++++++++++++++++++++ fetch/transport_tinygo.go | 22 +++++++ fetch/transport_tinygo_test.go | 37 ++++++++++++ safehttp/safehttp.go | 34 +---------- safehttp/transport_std.go | 45 +++++++++++++++ safehttp/transport_tinygo.go | 25 ++++++++ safehttp/transport_tinygo_test.go | 51 ++++++++++++++++ 13 files changed, 457 insertions(+), 118 deletions(-) create mode 100644 client/transport_portable_test.go create mode 100644 client/transport_tinygo_test.go create mode 100644 fetch/transport_portable_test.go create mode 100644 fetch/transport_std.go create mode 100644 fetch/transport_tinygo.go create mode 100644 fetch/transport_tinygo_test.go create mode 100644 safehttp/transport_std.go create mode 100644 safehttp/transport_tinygo.go create mode 100644 safehttp/transport_tinygo_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 818cdf5..4ff89a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,15 @@ jobs: - name: Test run: go test -v -race ./... + - name: Test TinyGo transport policy + run: go test -tags=tinygo -run 'TestTinyGo|TestPortable' ./fetch ./client ./safehttp + + - name: Build WebAssembly + run: GOOS=js GOARCH=wasm go build ./... + + - name: Build WASI + run: GOOS=wasip1 GOARCH=wasm go build ./... + lint: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 15b8b34..897b863 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,8 @@ statusCode, err := c.Head(ctx, "https://registry.npmjs.org/lodash") The `fetch` sub-package provides streaming artifact downloads with retry, circuit breaking, DNS caching, and URL resolution. +Under TinyGo, pass a host-compatible `*http.Client` through `fetch.WithHTTPClient`; the default fetch transport returns `errors.ErrUnsupported`. Registry API clients also accept a supplied client through `registries.WithHTTPClient`. The host must enforce address and redirect restrictions. `safehttp.New` and `WithSafeHTTP` return clients whose requests fail with `errors.ErrUnsupported`, since TinyGo transports bypass their dial-time address checks. The standalone IP-checking functions remain available. Native Go retains DNS caching and transport protection. + ### Fetching artifacts ```go diff --git a/client/transport_portable_test.go b/client/transport_portable_test.go new file mode 100644 index 0000000..951cdf1 --- /dev/null +++ b/client/transport_portable_test.go @@ -0,0 +1,46 @@ +package client_test + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/git-pkgs/registries" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestPortableRegistryWithHTTPClient(t *testing.T) { + calls := 0 + transport := roundTripFunc(func(request *http.Request) (*http.Response, error) { + calls++ + if request.URL.String() != "https://registry.example.test/demo" || request.Header.Get("Accept") != "application/json" { + t.Errorf("unexpected request: %s, headers: %v", request.URL, request.Header) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(`{ + "_id":"demo", "name":"demo", "description":"Fixture package", + "dist-tags":{"latest":"1.0.0"}, + "versions":{"1.0.0":{"name":"demo","version":"1.0.0","license":"MIT"}} + }`)), + }, nil + }) + client := registries.NewClient(registries.WithHTTPClient(&http.Client{Transport: transport})) + registry, err := registries.New("npm", "https://registry.example.test", client) + if err != nil { + t.Fatal(err) + } + pkg, err := registry.FetchPackage(context.Background(), "demo") + if err != nil { + t.Fatal(err) + } + if pkg.Name != "demo" || pkg.Description != "Fixture package" || calls != 1 { + t.Errorf("package = %+v, requests = %d", pkg, calls) + } +} diff --git a/client/transport_tinygo_test.go b/client/transport_tinygo_test.go new file mode 100644 index 0000000..18200e8 --- /dev/null +++ b/client/transport_tinygo_test.go @@ -0,0 +1,32 @@ +//go:build tinygo + +package client_test + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/git-pkgs/registries" +) + +func TestTinyGoWithSafeHTTP(t *testing.T) { + transport := roundTripFunc(func(*http.Request) (*http.Response, error) { + t.Error("WithSafeHTTP called the unprotected transport") + return nil, errors.New("unexpected request") + }) + client := registries.NewClient( + registries.WithHTTPClient(&http.Client{Transport: transport}), + registries.WithSafeHTTP(), + registries.WithMaxRetries(0), + ) + registry, err := registries.New("npm", "https://registry.example.test", client) + if err != nil { + t.Fatal(err) + } + _, err = registry.FetchPackage(context.Background(), "demo") + if !errors.Is(err, errors.ErrUnsupported) { + t.Errorf("FetchPackage error = %v, want ErrUnsupported", err) + } +} diff --git a/fetch/fetcher.go b/fetch/fetcher.go index a9df3eb..97ef572 100644 --- a/fetch/fetcher.go +++ b/fetch/fetcher.go @@ -9,32 +9,21 @@ import ( "io" "math" "math/rand" - "net" "net/http" "strconv" "time" - "github.com/rs/dnscache" - "github.com/git-pkgs/registries/safehttp" ) const ( - dnsRefreshInterval = 5 * time.Minute - dialTimeout = 30 * time.Second - dialKeepAlive = 30 * time.Second - httpClientTimeout = 5 * time.Minute - responseHeaderTimeout = 60 * time.Second - maxIdleConns = 100 - maxIdleConnsPerHost = 10 - idleConnTimeout = 90 * time.Second - tlsHandshakeTimeout = 10 * time.Second - defaultMaxRetries = 3 - defaultBaseDelay = 500 * time.Millisecond - backoffBase = 2 - jitterFactor = 0.1 - serverErrThreshold = 500 - maxErrBodySize = 1024 + httpClientTimeout = 5 * time.Minute + defaultMaxRetries = 3 + defaultBaseDelay = 500 * time.Millisecond + backoffBase = 2 + jitterFactor = 0.1 + serverErrThreshold = 500 + maxErrBodySize = 1024 ) var ( @@ -122,79 +111,14 @@ func WithAllowPrivateHosts(hosts ...string) Option { // NewFetcher creates a new Fetcher with the given options. // Callers should invoke Close when done to release the DNS refresh goroutine. +// Under TinyGo, requests require WithHTTPClient. func NewFetcher(opts ...Option) *Fetcher { - resolver := &dnscache.Resolver{} - stop := make(chan struct{}) - go func() { - ticker := time.NewTicker(dnsRefreshInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - resolver.Refresh(true) - case <-stop: - return - } - } - }() - - dialer := &net.Dialer{ - Timeout: dialTimeout, - KeepAlive: dialKeepAlive, - } - - var f *Fetcher - f = &Fetcher{ - client: &http.Client{ - Timeout: httpClientTimeout, - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - ips, err := resolver.LookupHost(ctx, host) - if err != nil { - return nil, err - } - // Gate every resolved IP against the safehttp block - // list (loopback, RFC1918, CGNAT, link-local, ...) - // before dialing. The dial is to the resolved IP - // directly so a rebind between gate and connect - // cannot escape. - var lastErr error - for _, ip := range ips { - if parsed := net.ParseIP(ip); parsed != nil { - if err := f.ipChecker.Check(host, parsed); err != nil { - lastErr = err - continue - } - } - conn, derr := dialer.DialContext(ctx, network, net.JoinHostPort(ip, port)) - if derr == nil { - return conn, nil - } - lastErr = derr - } - if lastErr == nil { - return nil, fmt.Errorf("no IPs resolved for %s", host) - } - return nil, fmt.Errorf("dialing %s: %w", host, lastErr) - }, - MaxIdleConns: maxIdleConns, - MaxIdleConnsPerHost: maxIdleConnsPerHost, - IdleConnTimeout: idleConnTimeout, - TLSHandshakeTimeout: tlsHandshakeTimeout, - ResponseHeaderTimeout: responseHeaderTimeout, - ExpectContinueTimeout: 1 * time.Second, - }, - }, + f := &Fetcher{ userAgent: "git-pkgs-proxy/1.0", maxRetries: defaultMaxRetries, baseDelay: defaultBaseDelay, - stop: stop, } + f.initHTTPClient() for _, opt := range opts { opt(f) } diff --git a/fetch/transport_portable_test.go b/fetch/transport_portable_test.go new file mode 100644 index 0000000..ba6ea43 --- /dev/null +++ b/fetch/transport_portable_test.go @@ -0,0 +1,88 @@ +package fetch_test + +import ( + "context" + "io" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/git-pkgs/registries/fetch" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestPortableFetcherWithHTTPClient(t *testing.T) { + const artifactURL = "https://repo.example.test/org/example/demo/1.0/demo-1.0.pom" + const content = `4.0.0org.exampledemo1.0` + var methods []string + transport := roundTripFunc(func(request *http.Request) (*http.Response, error) { + methods = append(methods, request.Method) + if request.URL.String() != artifactURL || request.Header.Get("Authorization") != "Bearer fixture-token" { + t.Errorf("unexpected request: %s, headers: %v", request.URL, request.Header) + } + body := content + if request.Method == http.MethodHead { + body = "" + } + return &http.Response{ + StatusCode: http.StatusOK, + Request: request, + Body: io.NopCloser(strings.NewReader(body)), + Header: http.Header{ + "Content-Length": {strconv.Itoa(len(content))}, + "Content-Type": {"application/xml"}, + "Etag": {`"fixture"`}, + }, + }, nil + }) + f := fetch.NewFetcher( + fetch.WithHTTPClient(&http.Client{Transport: transport}), + fetch.WithAuthFunc(func(string) (string, string) { return "Authorization", "Bearer fixture-token" }), + ) + t.Cleanup(func() { + if err := f.Close(); err != nil { + t.Error(err) + } + }) + + artifact, err := f.Fetch(context.Background(), artifactURL) + if err != nil { + t.Fatal(err) + } + checkArtifactBody(t, artifact, content) + + observed, err := f.FetchObserved(context.Background(), artifactURL) + if err != nil { + t.Fatal(err) + } + checkArtifactBody(t, observed.Artifact, content) + if !observed.Observation.Complete || observed.Observation.ByteCount != int64(len(content)) { + t.Errorf("observation = %+v", observed.Observation) + } + + size, contentType, err := f.Head(context.Background(), artifactURL) + if err != nil || size != int64(len(content)) || contentType != "application/xml" { + t.Errorf("Head = %d, %q, %v", size, contentType, err) + } + if got := strings.Join(methods, ","); got != "GET,GET,HEAD" { + t.Errorf("methods = %q", got) + } +} + +func checkArtifactBody(t *testing.T, artifact *fetch.Artifact, want string) { + t.Helper() + body, err := io.ReadAll(artifact.Body) + if closeErr := artifact.Body.Close(); closeErr != nil { + t.Error(closeErr) + } + if err != nil || string(body) != want { + t.Errorf("body = %q, %v", body, err) + } + if artifact.Size != int64(len(want)) || artifact.ContentType != "application/xml" || artifact.ETag != `"fixture"` { + t.Errorf("artifact = %+v", artifact) + } +} diff --git a/fetch/transport_std.go b/fetch/transport_std.go new file mode 100644 index 0000000..42b42c6 --- /dev/null +++ b/fetch/transport_std.go @@ -0,0 +1,88 @@ +//go:build !tinygo + +package fetch + +import ( + "context" + "fmt" + "net" + "net/http" + "time" + + "github.com/rs/dnscache" +) + +const ( + dnsRefreshInterval = 5 * time.Minute + dialTimeout = 30 * time.Second + dialKeepAlive = 30 * time.Second + responseHeaderTimeout = 60 * time.Second + maxIdleConns = 100 + maxIdleConnsPerHost = 10 + idleConnTimeout = 90 * time.Second + tlsHandshakeTimeout = 10 * time.Second +) + +func (f *Fetcher) initHTTPClient() { + resolver := &dnscache.Resolver{} + stop := make(chan struct{}) + f.stop = stop + go func() { + ticker := time.NewTicker(dnsRefreshInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + resolver.Refresh(true) + case <-stop: + return + } + } + }() + + dialer := &net.Dialer{ + Timeout: dialTimeout, + KeepAlive: dialKeepAlive, + } + f.client = &http.Client{ + Timeout: httpClientTimeout, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + ips, err := resolver.LookupHost(ctx, host) + if err != nil { + return nil, err + } + // Dial the checked IP directly to prevent DNS rebinding. + var lastErr error + for _, ip := range ips { + if parsed := net.ParseIP(ip); parsed != nil { + if err := f.ipChecker.Check(host, parsed); err != nil { + lastErr = err + continue + } + } + conn, derr := dialer.DialContext(ctx, network, net.JoinHostPort(ip, port)) + if derr == nil { + return conn, nil + } + lastErr = derr + } + if lastErr == nil { + return nil, fmt.Errorf("no IPs resolved for %s", host) + } + return nil, fmt.Errorf("dialing %s: %w", host, lastErr) + }, + MaxIdleConns: maxIdleConns, + MaxIdleConnsPerHost: maxIdleConnsPerHost, + IdleConnTimeout: idleConnTimeout, + TLSHandshakeTimeout: tlsHandshakeTimeout, + ResponseHeaderTimeout: responseHeaderTimeout, + ExpectContinueTimeout: 1 * time.Second, + }, + } +} diff --git a/fetch/transport_tinygo.go b/fetch/transport_tinygo.go new file mode 100644 index 0000000..dfd0324 --- /dev/null +++ b/fetch/transport_tinygo.go @@ -0,0 +1,22 @@ +//go:build tinygo + +package fetch + +import ( + "errors" + "fmt" + "net/http" +) + +func (f *Fetcher) initHTTPClient() { + f.client = &http.Client{ + Timeout: httpClientTimeout, + Transport: unsupportedTransport{}, + } +} + +type unsupportedTransport struct{} + +func (unsupportedTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, fmt.Errorf("fetch: TinyGo requires WithHTTPClient: %w", errors.ErrUnsupported) +} diff --git a/fetch/transport_tinygo_test.go b/fetch/transport_tinygo_test.go new file mode 100644 index 0000000..9bb0352 --- /dev/null +++ b/fetch/transport_tinygo_test.go @@ -0,0 +1,37 @@ +//go:build tinygo + +package fetch_test + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/git-pkgs/registries/fetch" +) + +func TestTinyGoFetcherRequiresHTTPClient(t *testing.T) { + original := http.DefaultTransport + http.DefaultTransport = roundTripFunc(func(*http.Request) (*http.Response, error) { + t.Error("default transport called without an injected client") + return nil, errors.New("unexpected request") + }) + t.Cleanup(func() { http.DefaultTransport = original }) + f := fetch.NewFetcher(fetch.WithAllowPrivateHosts("repo.example.test")) + ctx := context.Background() + const url = "https://repo.example.test/demo-1.0.pom" + _, fetchErr := f.Fetch(ctx, url) + _, observedErr := f.FetchObserved(ctx, url) + _, _, headErr := f.Head(ctx, url) + for _, err := range []error{fetchErr, observedErr, headErr} { + if !errors.Is(err, errors.ErrUnsupported) { + t.Errorf("request error = %v, want ErrUnsupported", err) + } + } + for range 2 { + if err := f.Close(); err != nil { + t.Errorf("Close: %v", err) + } + } +} diff --git a/safehttp/safehttp.go b/safehttp/safehttp.go index fadaff3..78d9035 100644 --- a/safehttp/safehttp.go +++ b/safehttp/safehttp.go @@ -74,39 +74,9 @@ func EnableLoopbackForTesting() { testInsecure = true } // New returns an http.Client that applies the SSRF defences described // in the package doc. base may be nil; if non-nil its Timeout, Jar, // and other non-Transport fields are preserved. +// Under TinyGo, requests return errors.ErrUnsupported. func New(base *http.Client, opts Options) *http.Client { - c := http.Client{Timeout: defaultTimeout} - if base != nil { - c = *base - } - - transport, _ := http.DefaultTransport.(*http.Transport) - transport = transport.Clone() - if base != nil { - if t, ok := base.Transport.(*http.Transport); ok && t != nil { - transport = t.Clone() - } - } - - underlying := transport.DialContext - if underlying == nil { - d := &net.Dialer{Timeout: dialTimeout} - underlying = d.DialContext - } - - gate := newGate(opts) - transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return gate.dial(ctx, network, addr, underlying) - } - c.Transport = transport - - c.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if len(via) >= MaxRedirects { - return fmt.Errorf("safehttp: stopped after %d redirects", MaxRedirects) - } - return validateRedirect(req.URL) - } - return &c + return newClient(base, opts) } // CheckIP reports whether an IP is acceptable to dial under the diff --git a/safehttp/transport_std.go b/safehttp/transport_std.go new file mode 100644 index 0000000..2244ec6 --- /dev/null +++ b/safehttp/transport_std.go @@ -0,0 +1,45 @@ +//go:build !tinygo + +package safehttp + +import ( + "context" + "fmt" + "net" + "net/http" +) + +func newClient(base *http.Client, opts Options) *http.Client { + c := http.Client{Timeout: defaultTimeout} + if base != nil { + c = *base + } + + transport, _ := http.DefaultTransport.(*http.Transport) + transport = transport.Clone() + if base != nil { + if t, ok := base.Transport.(*http.Transport); ok && t != nil { + transport = t.Clone() + } + } + + underlying := transport.DialContext + if underlying == nil { + d := &net.Dialer{Timeout: dialTimeout} + underlying = d.DialContext + } + + gate := newGate(opts) + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return gate.dial(ctx, network, addr, underlying) + } + c.Transport = transport + + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) >= MaxRedirects { + return fmt.Errorf("safehttp: stopped after %d redirects", MaxRedirects) + } + return validateRedirect(req.URL) + } + return &c +} diff --git a/safehttp/transport_tinygo.go b/safehttp/transport_tinygo.go new file mode 100644 index 0000000..802b076 --- /dev/null +++ b/safehttp/transport_tinygo.go @@ -0,0 +1,25 @@ +//go:build tinygo + +package safehttp + +import ( + "errors" + "fmt" + "net/http" +) + +func newClient(base *http.Client, _ Options) *http.Client { + c := http.Client{Timeout: defaultTimeout} + if base != nil { + c = *base + } + // TinyGo transports bypass the dialer that enforces the address policy. + c.Transport = unsupportedTransport{} + return &c +} + +type unsupportedTransport struct{} + +func (unsupportedTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, fmt.Errorf("safehttp: TinyGo cannot enforce the address policy: %w", errors.ErrUnsupported) +} diff --git a/safehttp/transport_tinygo_test.go b/safehttp/transport_tinygo_test.go new file mode 100644 index 0000000..3c5e510 --- /dev/null +++ b/safehttp/transport_tinygo_test.go @@ -0,0 +1,51 @@ +//go:build tinygo + +package safehttp_test + +import ( + "errors" + "net" + "net/http" + "testing" + "time" + + "github.com/git-pkgs/registries/safehttp" +) + +type rejectingTransport struct{ t *testing.T } + +func (r rejectingTransport) RoundTrip(*http.Request) (*http.Response, error) { + r.t.Error("unprotected transport called") + return nil, errors.New("unexpected request") +} + +func TestTinyGoSafeHTTPRejectsRequests(t *testing.T) { + transport := rejectingTransport{t} + original := http.DefaultTransport + http.DefaultTransport = transport + t.Cleanup(func() { http.DefaultTransport = original }) + base := &http.Client{Timeout: time.Second, Transport: transport} + for _, input := range []*http.Client{nil, base} { + for _, opts := range []safehttp.Options{{}, {AllowLoopback: true, AllowPrivate: true, AllowPrivateHosts: []string{"repo.example.test"}}} { + client := safehttp.New(input, opts) + _, err := client.Get("https://repo.example.test/demo") + if !errors.Is(err, errors.ErrUnsupported) { + t.Errorf("Get error = %v, want ErrUnsupported", err) + } + if input != nil && (client == input || client.Timeout != input.Timeout || input.Transport != transport) { + t.Error("New must copy the client without modifying its settings") + } + } + } +} + +func TestTinyGoSafeHTTPAddressChecks(t *testing.T) { + if err := safehttp.CheckIP(net.ParseIP("127.0.0.1"), safehttp.Options{}); err == nil { + t.Error("CheckIP accepted loopback") + } + if err := safehttp.CheckHostIP("repo.example.test", net.ParseIP("10.0.0.1"), safehttp.Options{ + AllowPrivateHosts: []string{"repo.example.test"}, + }); err != nil { + t.Errorf("CheckHostIP rejected an allowed private host: %v", err) + } +} From 5f99d43c3cd09977721b4d5cbfd05afea4a2ae23 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 26 Sep 2026 21:15:00 +0100 Subject: [PATCH 2/2] Run supplied client tests with TinyGo in CI --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ff89a1..a8cb9ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,44 @@ jobs: - name: Build WASI run: GOOS=wasip1 GOARCH=wasm go build ./... + tinygo: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Set up Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + package-manager-cache: false + + - name: Install TinyGo and Wasmtime + working-directory: ${{ runner.temp }} + run: | + curl --fail --location --silent --show-error --output tinygo.tar.gz https://github.com/tinygo-org/tinygo/releases/download/v0.42.0/tinygo0.42.0.linux-amd64.tar.gz + echo 'b87688fa2e19cee7d813cad7fd7dadb71dff3198e47125aba66ba4af5e490438 tinygo.tar.gz' | sha256sum --check + tar -xzf tinygo.tar.gz + echo "$RUNNER_TEMP/tinygo/bin" >> "$GITHUB_PATH" + curl --fail --location --silent --show-error --output wasmtime.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-linux.tar.xz + echo 'afd58715f105e3a7f454169daed22168c5736ec5f225fb04c4ac62c54c9508a3 wasmtime.tar.xz' | sha256sum --check + tar -xJf wasmtime.tar.xz + echo "$RUNNER_TEMP/wasmtime-v44.0.1-x86_64-linux" >> "$GITHUB_PATH" + + - name: Test TinyGo WebAssembly supplied clients and transport policy + run: tinygo test -target=wasm -run 'TestTinyGo|TestPortable' -v ./fetch ./client ./safehttp + + - name: Test TinyGo WASI supplied clients and transport policy + run: tinygo test -target=wasip1 -run 'TestTinyGo|TestPortable' -v ./fetch ./client ./safehttp + lint: runs-on: ubuntu-latest steps: