Scaffold build repo for pinned tailscale/go-cache-plugin
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

Upstream publishes no releases, so CI has nothing to curl.

- pin cmd/go-cache-plugin at 3031b5d via the go.mod tool directive
- build a static stripped binary with make build
- test the built binary: static ELF, pinned provenance, and a real
  go build driven through it as a GOCACHEPROG against a stub S3
- add PR build/test/pre-commit workflows and a tag release workflow
  attaching linux/amd64 and linux/arm64 to a Gitea release
This commit is contained in:
2026-09-26 21:30:07 +10:00
parent b7905873c8
commit 3e2f7e59b6
11 changed files with 590 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
dist/
/go-cache-plugin
/go-cache-plugin-*
+15
View File
@@ -0,0 +1,15 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: mixed-line-ending
args: [--fix=lf]
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
+18
View File
@@ -0,0 +1,18 @@
when:
- event: pull_request
steps:
- name: build
image: docker.io/golang:1.26
commands:
- make build
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 2Gi
cpu: 2
limits:
memory: 6Gi
cpu: 4
+18
View File
@@ -0,0 +1,18 @@
when:
- event: pull_request
steps:
- name: pre-commit
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
commands:
- uvx pre-commit run --all-files
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+69
View File
@@ -0,0 +1,69 @@
when:
- event: tag
steps:
- name: test
image: docker.io/golang:1.26
commands:
- make test
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 2Gi
cpu: 2
limits:
memory: 6Gi
cpu: 4
# Static binaries so CI images can curl one and run it regardless of libc.
- name: build
image: docker.io/golang:1.26
commands:
- |
for arch in amd64 arm64; do
make build OS=linux ARCH="$$arch"
mv dist/go-cache-plugin "go-cache-plugin-linux-$$arch"
done
- make upstream > upstream-version.txt
depends_on: [test]
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 2Gi
cpu: 2
limits:
memory: 6Gi
cpu: 4
- name: release
image: git.unkin.net/unkin/almalinux9-base:20260606
environment:
RELEASER_TOKEN:
from_secret: RELEASER_TOKEN
commands:
# $$ escapes shell vars so Woodpecker leaves them for the shell;
# ${CI_COMMIT_TAG}/${CI_REPO} are real Woodpecker vars.
- |
curl -f -o /usr/local/bin/tea https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/gitea-dl/tea/0.12.0/tea-0.12.0-linux-amd64
chmod +x /usr/local/bin/tea
tea logins add --name gitea --url https://git.unkin.net --token "$${RELEASER_TOKEN}" --no-version-check
ASSETS="go-cache-plugin-linux-amd64 go-cache-plugin-linux-arm64"
sha256sum $$ASSETS > sha256sums.txt
NOTES="Built from github.com/tailscale/go-cache-plugin $$(cat upstream-version.txt)"
tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "$$NOTES" --login gitea --repo "${CI_REPO}"
tea releases assets create "${CI_COMMIT_TAG}" $$ASSETS sha256sums.txt --login gitea --repo "${CI_REPO}"
depends_on: [build]
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
+37
View File
@@ -0,0 +1,37 @@
when:
- event: pull_request
steps:
# go vet rather than golangci-lint: the repo has no source of its own beyond
# the build test, and the lint image lags the Go version upstream requires.
- name: vet
image: docker.io/golang:1.26
commands:
- go vet ./...
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 2Gi
cpu: 2
limits:
memory: 6Gi
cpu: 4
# Builds the pinned binary and drives a real `go build` through it.
- name: test
image: docker.io/golang:1.26
commands:
- make test
depends_on: [vet]
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 2Gi
cpu: 2
limits:
memory: 6Gi
cpu: 4
+58
View File
@@ -0,0 +1,58 @@
BINARY := go-cache-plugin
# Upstream has no releases; the shipped binary is built from the module version
# pinned by the tool directive in go.mod.
PKG := github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin
DIST := dist
UPSTREAM := $(shell go list -m -f '{{.Version}}' github.com/tailscale/go-cache-plugin)
GOFLAGS := -ldflags="-s -w"
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
.PHONY: all build test lint fmt pre-commit clean upstream patch minor major _tag
all: build
# Static so the released binary runs on any image in the estate, Alpine included.
build:
CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build $(GOFLAGS) -o $(DIST)/$(BINARY) $(PKG)
test:
go test -v ./...
lint:
golangci-lint run ./...
fmt:
gofmt -w .
pre-commit:
uvx pre-commit run --all-files
upstream:
@echo $(UPSTREAM)
clean:
rm -rf $(DIST)
# Bump helpers -- reads the latest semver tag and creates the next one.
# If no tag exists yet, starts from v0.0.0.
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
patch:
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _tag TAG=$$NEW
minor:
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _tag TAG=$$NEW
major:
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _tag TAG=$$NEW
_tag:
git push origin $(TAG)
+56 -1
View File
@@ -1,3 +1,58 @@
# go-cache-plugin
Build repo that compiles the tailscale/go-cache-plugin GOCACHEPROG S3 Go build cache from a pinned upstream module version and publishes static binaries as Gitea releases.
Build repo for [`tailscale/go-cache-plugin`](https://github.com/tailscale/go-cache-plugin),
a `GOCACHEPROG` implementation that backs the Go build cache with S3. Upstream
publishes no releases, so this repo pins a commit via the `tool` directive in
`go.mod` and ships the static binary as a Gitea release.
No source of its own. To move to a newer upstream commit:
```sh
go get -tool github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin@<commit>
make test
```
## Build
```sh
make build # dist/go-cache-plugin, CGO_ENABLED=0, stripped
make test # builds the binary, then drives a real `go build` through it
```
## Use
Point it at the estate's Ceph RGW, which needs both a custom endpoint and
path-style addressing:
```sh
export GOCACHEPROG="go-cache-plugin \
--cache-dir=$HOME/.cache/go-cache-plugin \
--bucket=go-build-cache \
--region=default \
--s3-endpoint-url=https://s3.ceph.unkin.net \
--s3-path-style"
go build ./...
```
Credentials come from the usual `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`
environment. Every flag also has a `GOCACHE_*` environment equivalent — see
`go-cache-plugin help environment`. Add `--metrics` to print cache hit rates at
exit, and `--expiry` to age entries out.
For a shared cache daemon, run `go-cache-plugin serve --plugin <port>` once and
have each build use `go-cache-plugin connect <port>` as its `GOCACHEPROG`.
## CI
Woodpecker steps fetch the released binary:
```yaml
commands:
- curl -fsSLo /usr/local/bin/go-cache-plugin https://git.unkin.net/unkin/go-cache-plugin/releases/download/v0.1.0/go-cache-plugin-linux-amd64
- chmod +x /usr/local/bin/go-cache-plugin
```
## Release
`make patch|minor|major` tags and pushes; the tag pipeline builds
`linux/amd64` and `linux/arm64` and attaches them with `sha256sums.txt`.
+191
View File
@@ -0,0 +1,191 @@
// Package gocacheplugin has no source of its own: this repo exists only to
// build the pinned upstream go-cache-plugin and ship it as a release. The tests
// therefore exercise the built binary rather than any local code.
package gocacheplugin
import (
"debug/buildinfo"
"debug/elf"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
"testing"
)
const (
upstreamPkg = "github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin"
upstreamModule = "github.com/tailscale/go-cache-plugin"
)
var pluginBin string
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "go-cache-plugin-test")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
pluginBin = filepath.Join(dir, "go-cache-plugin")
build := exec.Command("go", "build", "-ldflags=-s -w", "-o", pluginBin, upstreamPkg)
build.Env = append(os.Environ(), "CGO_ENABLED=0")
if out, err := build.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "building %s: %v\n%s", upstreamPkg, err, out)
_ = os.RemoveAll(dir)
os.Exit(1)
}
code := m.Run()
_ = os.RemoveAll(dir)
os.Exit(code)
}
// A dynamically linked binary will not run on the estate's Alpine-based CI
// images, so CGO_ENABLED=0 must actually take effect.
func TestBinaryIsStatic(t *testing.T) {
fi, err := os.Stat(pluginBin)
if err != nil {
t.Fatal(err)
}
if fi.Size() == 0 {
t.Fatal("binary is empty")
}
f, err := elf.Open(pluginBin)
if err != nil {
t.Fatal(err)
}
defer func() { _ = f.Close() }()
if s := f.Section(".interp"); s != nil {
t.Error("binary has a .interp section, so it needs a dynamic loader")
}
libs, err := f.ImportedLibraries()
if err != nil {
t.Fatal(err)
}
if len(libs) != 0 {
t.Errorf("binary links shared libraries: %v", libs)
}
}
// The binary must come from the version go.mod pins, not from whatever the
// toolchain would resolve on its own.
func TestBuiltFromPinnedVersion(t *testing.T) {
out, err := exec.Command("go", "list", "-m", "-f", "{{.Version}}", upstreamModule).Output()
if err != nil {
t.Fatal(err)
}
want := strings.TrimSpace(string(out))
if want == "" {
t.Fatalf("%s is not in the module graph", upstreamModule)
}
info, err := buildinfo.ReadFile(pluginBin)
if err != nil {
t.Fatal(err)
}
got := info.Main.Version
for _, dep := range info.Deps {
if dep.Path == upstreamModule {
got = dep.Version
}
}
if got != want {
t.Errorf("binary built from %s %q, go.mod pins %q", upstreamModule, got, want)
}
}
// Sanity check that the real entry point runs and parses flags: without a
// bucket the cache server must refuse to start.
func TestRequiresBucket(t *testing.T) {
cmd := exec.Command(pluginBin, "--cache-dir="+t.TempDir())
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected a non-zero exit, got success:\n%s", out)
}
if !strings.Contains(string(out), "--bucket") {
t.Errorf("expected a usage error naming --bucket, got:\n%s", out)
}
}
// End to end: drive a real `go build` through the binary as a GOCACHEPROG
// plugin, pointed at a stub S3 endpoint that misses every read. This is the
// only thing the repo ships, so it has to actually speak the protocol.
func TestServesGoCacheProg(t *testing.T) {
var reqs atomic.Int64
s3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqs.Add(1)
if r.Method == http.MethodPut || r.Method == http.MethodPost {
_, _ = io.Copy(io.Discard, r.Body)
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusNotFound)
_, _ = io.WriteString(w, `<?xml version="1.0" encoding="UTF-8"?><Error><Code>NoSuchKey</Code><Message>no such key</Message></Error>`)
}))
defer s3.Close()
cacheDir := t.TempDir()
src := t.TempDir()
write(t, filepath.Join(src, "go.mod"), "module example.test\n\ngo 1.26.1\n")
write(t, filepath.Join(src, "main.go"), "package main\n\nfunc main() { println(\"ok\") }\n")
prog := strings.Join([]string{
pluginBin,
"--cache-dir=" + cacheDir,
"--bucket=go-cache-test",
"--region=us-east-1",
"--s3-endpoint-url=" + s3.URL,
"--s3-path-style",
}, " ")
build := exec.Command("go", "build", "-o", filepath.Join(src, "out"), ".")
build.Dir = src
build.Env = append(os.Environ(),
"GOFLAGS=",
"GOCACHEPROG="+prog,
"AWS_ACCESS_KEY_ID=test",
"AWS_SECRET_ACCESS_KEY=test",
)
if out, err := build.CombinedOutput(); err != nil {
t.Fatalf("go build under GOCACHEPROG failed: %v\n%s", err, out)
}
if _, err := os.Stat(filepath.Join(src, "out")); err != nil {
t.Fatalf("build produced no binary: %v", err)
}
if n := countFiles(t, cacheDir); n == 0 {
t.Error("plugin staged nothing in the local cache directory")
}
if reqs.Load() == 0 {
t.Error("plugin made no S3 requests")
}
}
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func countFiles(t *testing.T, dir string) int {
t.Helper()
var n int
err := filepath.WalkDir(dir, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
n++
}
return nil
})
if err != nil {
t.Fatal(err)
}
return n
}
+47
View File
@@ -0,0 +1,47 @@
module git.unkin.net/unkin/go-cache-plugin
go 1.26.1
tool github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin
require (
github.com/aws/aws-sdk-go-v2 v1.41.6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.16 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect
github.com/aws/smithy-go v1.25.0 // indirect
github.com/creachadair/atomicfile v0.4.2 // indirect
github.com/creachadair/command v0.2.6 // indirect
github.com/creachadair/flax v0.0.6 // indirect
github.com/creachadair/gocache v0.0.0-20260710224145-042386578e2a // indirect
github.com/creachadair/mds v0.29.2 // indirect
github.com/creachadair/mhttp v0.0.0-20260407161248-6866af4c556b // indirect
github.com/creachadair/msync v0.8.1 // indirect
github.com/creachadair/scheddle v0.0.0-20260418161627-87a4a0c853c4 // indirect
github.com/creachadair/taskgroup v0.14.4 // indirect
github.com/creachadair/tlsutil v0.0.0-20260218173745-49b0059fedaf // indirect
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/goproxy/goproxy v0.21.0 // indirect
github.com/tailscale/go-cache-plugin v0.1.2-0.20260723040932-3031b5d01c50 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
tailscale.com v1.96.5 // indirect
)
+78
View File
@@ -0,0 +1,78 @@
github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg=
github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM=
github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 h1:xnvDEnw+pnj5mctWiYuFbigrEzSm35x7k4KS/ZkCANg=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14/go.mod h1:yS5rNogD8e0Wu9+l3MUwr6eENBzEeGejvINpN5PAYfY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 h1:SE+aQ4DEqG53RRCAIHlCf//B2ycxGH7jFkpnAh/kKPM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22/go.mod h1:ES3ynECd7fYeJIL6+oax+uIEljmfps0S70BaQzbMd/o=
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 h1:kU/eBN5+MWNo/LcbNa4hWDdN76hdcd7hocU5kvu7IsU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1/go.mod h1:Fw9aqhJicIVee1VytBBjH+l+5ov6/PhbtIK/u3rt/ls=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo=
github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U=
github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/creachadair/atomicfile v0.4.2 h1:pSSsKxmUefqCTg+QgyCG2bw1KmsB76apBL4lhJjichI=
github.com/creachadair/atomicfile v0.4.2/go.mod h1:ts9VunJluQvSXtkrl0YlDn9ckZaS+eMkBW2g20gB5II=
github.com/creachadair/command v0.2.6 h1:+d4xUSZza5nqRVVsVRf5jDbix/0uKxVqFrc30F4uyRI=
github.com/creachadair/command v0.2.6/go.mod h1:HWvS3zEpVakKlNXxT/j1U8tNoDaY7By7ORYQ+m1ha2U=
github.com/creachadair/flax v0.0.6 h1:IFUdRdfKynNTyR5SRlrHMUGnxxMZZen8RVpwcDq/ftk=
github.com/creachadair/flax v0.0.6/go.mod h1:F1PML0JZLXSNDMNiRGK2yjm5f+L9QCHchyHBldFymj8=
github.com/creachadair/gocache v0.0.0-20260710224145-042386578e2a h1:cYAmYaIvlHjbnpM2DmH+kNaOC3qYQcpg8iYXNaMrcMg=
github.com/creachadair/gocache v0.0.0-20260710224145-042386578e2a/go.mod h1:f7A5Y4U3AWm53Dh6diUnOxDRBfuFNLqusf07sLd6cc4=
github.com/creachadair/mds v0.29.2 h1:1IBhfyolZUCA53twI1hXembOdpIe4bJ4OGJELBbGrN4=
github.com/creachadair/mds v0.29.2/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI=
github.com/creachadair/mhttp v0.0.0-20260407161248-6866af4c556b h1:bIvrALyE2ixkCvz9LADfOIuhkUMYWMBsvQ29xe70nI0=
github.com/creachadair/mhttp v0.0.0-20260407161248-6866af4c556b/go.mod h1:eV+igunEfCBbgpM0gkDK/25/ukdeNYXH9PZuuEWZTDo=
github.com/creachadair/msync v0.8.1 h1:QRd8si3qZ2Q4TaDL7tS/MG/lFE3YND7U7J9fy42eAFM=
github.com/creachadair/msync v0.8.1/go.mod h1:dt0bscS09J8Ie3AdccK9JpCb7LfStaDGlAmDLukOlY4=
github.com/creachadair/scheddle v0.0.0-20260418161627-87a4a0c853c4 h1:uYu6k39F6Sz4Y5Zb+R2ZnhE1S5ehCw4lknwrt4bm7NI=
github.com/creachadair/scheddle v0.0.0-20260418161627-87a4a0c853c4/go.mod h1:PE4owEX+08lTR1VNomiSFFh6DzkmmzlBgysEOq8zwdI=
github.com/creachadair/taskgroup v0.14.4 h1:ttR8StLWmYA1O6x96YlTpQLXjKyRpO4RBo+OcO6W6C0=
github.com/creachadair/taskgroup v0.14.4/go.mod h1:uhCtIEsa7zpeMAFixddhCYjbJi7e4JMyU7OXUygkSsg=
github.com/creachadair/tlsutil v0.0.0-20260218173745-49b0059fedaf h1:6ZqacBCoIdOSlwh03zr3TMqA5VyuFpzUS7E2yzfjq7w=
github.com/creachadair/tlsutil v0.0.0-20260218173745-49b0059fedaf/go.mod h1:YUng4wUBh60nfswe0FwLeBOpoIsrW6ReXt9Zr9k55Sg=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/goproxy/goproxy v0.21.0 h1:S3JPCp/WLqLl7X40eo5FFiAbBZwD8S6TzGPBITwfHlo=
github.com/goproxy/goproxy v0.21.0/go.mod h1:4j3iRV76B133PK4sOzVYWSBs/SsE5ovbZsdceoC5f+w=
github.com/tailscale/go-cache-plugin v0.1.2-0.20260723040932-3031b5d01c50 h1:iF7+CaT5ro+WYnIZH6a9EbGPQ3KRWnF7VnyA+57sFKY=
github.com/tailscale/go-cache-plugin v0.1.2-0.20260723040932-3031b5d01c50/go.mod h1:HiHE7c3OvFT6JfCq+UZCupZN1aohtpYaY6MdhOpBPys=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=