From 58b19bc13b882b8ae665035ce7d56352f03b09c9 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 20:18:43 -0400 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20v0.1.0=20scaffold=20=E2=80=94=20g?= =?UTF-8?q?olang-migrate=20+=20goose=20drivers,=20module=20types,=20steps,?= =?UTF-8?q?=20conformance=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks 22-26: scaffold new workflow-plugin-migrations repo with: - go.mod (github.com/GoCodeAlone/workflow v0.18.2) - .goreleaser.yaml: three builds (migrations, atlas-migrate, workflow-migrate) - .github/workflows: CI (postgres service) + release - pkg/driver: MigrationDriver adapter + in-process registry - internal/module_migrations.go: database.migrations module type - internal/module_driver.go: database.migration_driver module type - internal/steps: step.migrate_up/down/status/to pipeline steps - internal/golangmigrate: golang-migrate driver implementation - internal/goose: goose driver implementation - internal/atlasplugin: stub for Atlas driver (Task 27) - pkg/testharness: embedded-postgres + provided-DSN harness - pkg/conformance: exported Suite with 6 behavioural cases + corpus SQL - pkg/cli: shared Cobra root for wfctl migrate * / workflow-migrate - cmd/workflow-plugin-migrations: main binary (golang-migrate + goose + CLI) - cmd/workflow-plugin-atlas-migrate: atlas-only binary - cmd/workflow-migrate: standalone pre-deploy binary - plugin.json: declares moduleTypes, stepTypes, migrationDrivers, cliCommands All short tests pass. Integration tests skip when no Postgres is available and run in CI via the postgres service container. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 37 ++ .github/workflows/release.yml | 28 + .gitignore | 4 + .goreleaser.yaml | 95 +++ cmd/workflow-migrate/main.go | 28 + cmd/workflow-plugin-atlas-migrate/main.go | 14 + cmd/workflow-plugin-migrations/main.go | 18 + go.mod | 202 ++++++ go.sum | 623 ++++++++++++++++++ internal/atlasplugin/plugin.go | 49 ++ internal/golangmigrate/driver.go | 168 +++++ internal/golangmigrate/driver_test.go | 109 +++ internal/goose/driver.go | 215 ++++++ internal/goose/driver_test.go | 101 +++ internal/module_driver.go | 52 ++ internal/module_driver_test.go | 32 + internal/module_migrations.go | 194 ++++++ internal/module_migrations_test.go | 75 +++ internal/plugin.go | 80 +++ internal/plugin_test.go | 114 ++++ internal/steps/base.go | 263 ++++++++ internal/steps/base_test.go | 72 ++ pkg/cli/root.go | 192 ++++++ .../corpus/001_create_users.down.sql | 1 + .../corpus/001_create_users.up.sql | 5 + .../corpus/002_add_indexes.down.sql | 2 + pkg/conformance/corpus/002_add_indexes.up.sql | 2 + pkg/conformance/suite.go | 212 ++++++ pkg/conformance/suite_test.go | 38 ++ pkg/driver/driver.go | 60 ++ pkg/driver/registry.go | 63 ++ pkg/testharness/harness.go | 54 ++ plugin.json | 43 ++ 33 files changed, 3245 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 cmd/workflow-migrate/main.go create mode 100644 cmd/workflow-plugin-atlas-migrate/main.go create mode 100644 cmd/workflow-plugin-migrations/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/atlasplugin/plugin.go create mode 100644 internal/golangmigrate/driver.go create mode 100644 internal/golangmigrate/driver_test.go create mode 100644 internal/goose/driver.go create mode 100644 internal/goose/driver_test.go create mode 100644 internal/module_driver.go create mode 100644 internal/module_driver_test.go create mode 100644 internal/module_migrations.go create mode 100644 internal/module_migrations_test.go create mode 100644 internal/plugin.go create mode 100644 internal/plugin_test.go create mode 100644 internal/steps/base.go create mode 100644 internal/steps/base_test.go create mode 100644 pkg/cli/root.go create mode 100644 pkg/conformance/corpus/001_create_users.down.sql create mode 100644 pkg/conformance/corpus/001_create_users.up.sql create mode 100644 pkg/conformance/corpus/002_add_indexes.down.sql create mode 100644 pkg/conformance/corpus/002_add_indexes.up.sql create mode 100644 pkg/conformance/suite.go create mode 100644 pkg/conformance/suite_test.go create mode 100644 pkg/driver/driver.go create mode 100644 pkg/driver/registry.go create mode 100644 pkg/testharness/harness.go create mode 100644 plugin.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1e285be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: migrations_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + WORKFLOW_MIGRATE_TEST_DSN: postgres://test:test@localhost:5432/migrations_test?sslmode=disable + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - run: go build ./... + - run: go test ./... -v -race -count=1 + - run: go vet ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5a9932e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + id-token: write # for OIDC/cosign keyless signing (future) + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - uses: goreleaser/goreleaser-action@v7 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f4a16b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +*.bak +*.test +vendor/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..5803656 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,95 @@ +version: 2 + +before: + hooks: + - go mod tidy + - "sed -i.bak 's/\"version\": \".*\"/\"version\": \"{{ .Version }}\"/' plugin.json && rm -f plugin.json.bak" + +builds: + # Main plugin binary: golang-migrate + goose + wfctl CLI + - id: workflow-plugin-migrations + main: ./cmd/workflow-plugin-migrations + binary: workflow-plugin-migrations + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X github.com/GoCodeAlone/workflow-plugin-migrations/internal.Version={{.Version}} + + # Atlas driver binary: atlas only (separate to keep main binary lean) + - id: workflow-plugin-atlas-migrate + main: ./cmd/workflow-plugin-atlas-migrate + binary: workflow-plugin-atlas-migrate + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X github.com/GoCodeAlone/workflow-plugin-migrations/internal/atlasplugin.Version={{.Version}} + + # Standalone migration binary for pre-deploy jobs + - id: workflow-migrate + main: ./cmd/workflow-migrate + binary: workflow-migrate + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + +archives: + - id: workflow-plugin-migrations + builds: [workflow-plugin-migrations] + formats: [tar.gz] + name_template: "workflow-plugin-migrations-{{ .Os }}-{{ .Arch }}" + files: + - plugin.json + - LICENSE + + - id: workflow-plugin-atlas-migrate + builds: [workflow-plugin-atlas-migrate] + formats: [tar.gz] + name_template: "workflow-plugin-atlas-migrate-{{ .Os }}-{{ .Arch }}" + files: + - plugin.json + - LICENSE + + - id: workflow-migrate + builds: [workflow-migrate] + formats: [tar.gz] + name_template: "workflow-migrate-{{ .Os }}-{{ .Arch }}" + files: + - LICENSE + +sboms: + - artifacts: archive + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^chore:" diff --git a/cmd/workflow-migrate/main.go b/cmd/workflow-migrate/main.go new file mode 100644 index 0000000..22c02cf --- /dev/null +++ b/cmd/workflow-migrate/main.go @@ -0,0 +1,28 @@ +// Command workflow-migrate is a standalone binary for running database migrations +// as a pre-deploy job in DO App Platform, Kubernetes, or any OCI-compatible runtime. +// It applies golang-migrate or goose migrations against a Postgres database. +// +// Usage: +// +// workflow-migrate up [flags] +// workflow-migrate down [flags] +// workflow-migrate status [flags] +// workflow-migrate goto [flags] +package main + +import ( + "fmt" + "os" + + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/cli" +) + +func main() { + root := cli.NewRoot() + root.Use = "workflow-migrate" + root.Short = "Standalone migration runner for pre-deploy jobs" + if err := root.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/workflow-plugin-atlas-migrate/main.go b/cmd/workflow-plugin-atlas-migrate/main.go new file mode 100644 index 0000000..2f27377 --- /dev/null +++ b/cmd/workflow-plugin-atlas-migrate/main.go @@ -0,0 +1,14 @@ +// Command workflow-plugin-atlas-migrate is a workflow engine external plugin +// that provides the Atlas migration driver only. Atlas has heavy dependencies +// (HCL toolchain) so it is published as a separate binary to keep the main +// plugin binary minimal. +package main + +import ( + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/atlasplugin" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +func main() { + sdk.Serve(atlasplugin.NewPlugin()) +} diff --git a/cmd/workflow-plugin-migrations/main.go b/cmd/workflow-plugin-migrations/main.go new file mode 100644 index 0000000..90be3f0 --- /dev/null +++ b/cmd/workflow-plugin-migrations/main.go @@ -0,0 +1,18 @@ +// Command workflow-plugin-migrations is a workflow engine external plugin +// that provides golang-migrate and goose database migration drivers, +// module types (database.migrations, database.migration_driver), and +// pipeline step types (step.migrate_up/down/status/to). +// +// When invoked with --wfctl-cli, it serves as the entrypoint for +// `wfctl migrate *` dynamic CLI commands. +package main + +import ( + "github.com/GoCodeAlone/workflow-plugin-migrations/internal" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/cli" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +func main() { + sdk.ServePluginFull(internal.NewPlugin(), cli.NewCLIProvider(), nil) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e9b47ea --- /dev/null +++ b/go.mod @@ -0,0 +1,202 @@ +module github.com/GoCodeAlone/workflow-plugin-migrations + +go 1.26.0 + +require ( + github.com/GoCodeAlone/workflow v0.18.2 + github.com/fergusstrange/embedded-postgres v1.34.0 + github.com/golang-migrate/migrate/v4 v4.19.1 + github.com/jackc/pgx/v5 v5.9.2 + github.com/pressly/goose/v3 v3.27.0 + github.com/spf13/cobra v1.10.2 +) + +require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.19.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.61.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/DataDog/datadog-go/v5 v5.8.3 // indirect + github.com/GoCodeAlone/go-plugin v1.7.0 // indirect + github.com/GoCodeAlone/modular v1.13.0 // indirect + github.com/GoCodeAlone/modular/modules/auth v1.15.0 // indirect + github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 // indirect + github.com/GoCodeAlone/yaegi v0.17.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/IBM/sarama v1.47.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8 // indirect + github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.13 // indirect + github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.81.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/digitalocean/godo v1.178.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/expr-lang/expr v1.17.8 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/vault/api v1.23.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/gojq v0.12.18 // indirect + github.com/itchyny/timefmt-go v0.1.7 // indirect + github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.50.0 // indirect + github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/zalando/go-keyring v0.2.8 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/api v0.272.0 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.47.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9f35900 --- /dev/null +++ b/go.sum @@ -0,0 +1,623 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ= +cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= +cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/datadog-go/v5 v5.8.3 h1:s58CUJ9s8lezjhTNJO/SxkPBv2qZjS3ktpRSqGF5n0s= +github.com/DataDog/datadog-go/v5 v5.8.3/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= +github.com/GoCodeAlone/go-plugin v1.7.0 h1:EwnhqPlXiNmp85S+MXnKKvm3YlfA6O4NzBb4+GSlEVY= +github.com/GoCodeAlone/go-plugin v1.7.0/go.mod h1:HbGQRZUIa+jbDfjsaZIMJYvrz+LnxL0mJpggfynSTMk= +github.com/GoCodeAlone/modular v1.13.0 h1:UfsegfAmPWcPYQOqYZFsw/LNySBmMDcthiOQe5bscqE= +github.com/GoCodeAlone/modular v1.13.0/go.mod h1:b06Pvgcc8HsGxvl30iO39zGH2jIWz467QEj2+OQL2Do= +github.com/GoCodeAlone/modular/modules/auth v1.15.0 h1:pBSkPSf4k4GLSbUQFLuPa+nFbfoJXGzSz9q89VoapZk= +github.com/GoCodeAlone/modular/modules/auth v1.15.0/go.mod h1:vmIm/LQrcURS2p02YwaELb+CZoHPtT0XB0v1i+sj9i4= +github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 h1:buYs0TGNbAZgtTq1Qb+dfmTv3+ZOBIN0HbvVBLyNqxE= +github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0/go.mod h1:329flAKmwrPq2JEwu9iltWv6A83H/Di82Xze+kvdKDw= +github.com/GoCodeAlone/workflow v0.18.2 h1:Vbm+NvhWMcHHl1zE6aQzNh3MSJzQhmunFO6DA3x9qh8= +github.com/GoCodeAlone/workflow v0.18.2/go.mod h1:ypkCqXTwnIPqNjS8h38KZfwzdVsgwgkS1d6Dq0lXyQQ= +github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= +github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= +github.com/IBM/sarama v1.47.0/go.mod h1:7gLLIU97nznOmA6TX++Qds+DRxH89P2XICY2KAQUzAY= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= +github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM= +github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8 h1:I0AMtyv5tqQ/VNDDalbbujALCWl64TP3F61bBw4U8Qs= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8/go.mod h1:qnrKR+Jzg9NbZqy+YusE7frSZUaYQ7EPJvki4+SwS3U= +github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.13 h1:juPaAcploym78WhVwleVHNLPmgURO6gkObC442Hal1s= +github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.13/go.mod h1:HjgDVqI6lGR0azGz1GKmZTzGHkXuzhKzRUfG/p5Ug8s= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12 h1:lQTVEv/YAk8Rw1Yf4XZS/jNNxF9klCN10WcSR3xlMtU= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12/go.mod h1:yoa0R6Xku788EmJYkFiARzJBxt4A3hgFjQPRmMAttr0= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumhqZrQfAeG8i4jPG6o= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0/go.mod h1:T6ndRfdhnXLIY5oKBHjYZDVj706los2zGdpThppquvA= +github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0 h1:a5G/TgJNrpuCjZBTf8/PTN0C2B0do/ylaYVynxPSbUQ= +github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0/go.mod h1:QkWmubOYmjj3cHn7A4CoUU7BKJhVeo39Gp6NH7IyhZw= +github.com/aws/aws-sdk-go-v2/service/eks v1.81.2 h1:6c/Jkyx1gYLiZGl6VPjApViaoPiYo7TDWXCMk/ZBq6c= +github.com/aws/aws-sdk-go-v2/service/eks v1.81.2/go.mod h1:xdUh6tdF9A8hc+PE84kmHbF/zsVPNiKnc6oLgulq1Eo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 h1:3m9iJtMtLq75jKRAfw0kapoHUlbzi0CRVigysBN/FHA= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4/go.mod h1:O2L6vGm4xacEuN2otHFMgn7yXXlgzFKzxrba0fy/yk8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 h1:Z+/OLsb85Kpq7TVLCspskqePaf68Tdv6GfmJP4kH6i0= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5/go.mod h1:TmxGowuBYwjmHFOsEDxaZdsQE62JJzOmtiWafTi/czg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM= +github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/digitalocean/godo v1.178.0 h1:+B4xGOaoFwwwpM7TKhoyGHdmFg5eF9zDB1YfOLvNJ2E= +github.com/digitalocean/godo v1.178.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= +github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fergusstrange/embedded-postgres v1.34.0 h1:c6RKhPKFsLVU+Tdxsx8q0UxCHsvZZ/iShAnljRBXs6s= +github.com/fergusstrange/embedded-postgres v1.34.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= +github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= +github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= +github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= +github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= +github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= +github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= +github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= +github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= +github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= +github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= +github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= +github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM= +github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/atlasplugin/plugin.go b/internal/atlasplugin/plugin.go new file mode 100644 index 0000000..9d60693 --- /dev/null +++ b/internal/atlasplugin/plugin.go @@ -0,0 +1,49 @@ +// Package atlasplugin provides the Atlas migration driver plugin. +// Atlas has larger dependencies (HCL toolchain) so it ships as a separate binary. +// This implementation will be fleshed out in Task 27. +package atlasplugin + +import ( + "fmt" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// Version is set at build time via -ldflags. +var Version = "0.0.0" + +// AtlasPlugin implements sdk.PluginProvider for the Atlas driver. +type AtlasPlugin struct{} + +// NewPlugin returns a new AtlasPlugin. +func NewPlugin() sdk.PluginProvider { + return &AtlasPlugin{} +} + +// Manifest returns plugin metadata. +func (p *AtlasPlugin) Manifest() sdk.PluginManifest { + return sdk.PluginManifest{ + Name: "workflow-plugin-atlas-migrate", + Version: Version, + Author: "GoCodeAlone", + Description: "Atlas HCL migration driver for the workflow engine", + } +} + +// ModuleTypes returns the module types this plugin provides. +func (p *AtlasPlugin) ModuleTypes() []string { + return []string{"database.migrations"} +} + +// CreateModule creates a module instance (atlas driver only). +func (p *AtlasPlugin) CreateModule(typeName, name string, _ map[string]any) (sdk.ModuleInstance, error) { + return nil, fmt.Errorf("atlasplugin: driver implementation coming in Task 27 (use workflow-plugin-migrations binary for golang-migrate/goose)") +} + +// StepTypes returns step type names. +func (p *AtlasPlugin) StepTypes() []string { return nil } + +// CreateStep creates a step instance. +func (p *AtlasPlugin) CreateStep(typeName, name string, _ map[string]any) (sdk.StepInstance, error) { + return nil, fmt.Errorf("atlasplugin: no steps provided") +} diff --git a/internal/golangmigrate/driver.go b/internal/golangmigrate/driver.go new file mode 100644 index 0000000..1955d9c --- /dev/null +++ b/internal/golangmigrate/driver.go @@ -0,0 +1,168 @@ +// Package golangmigrate provides a MigrationDriver backed by golang-migrate/migrate/v4. +package golangmigrate + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/pgx/v5" + _ "github.com/golang-migrate/migrate/v4/source/file" + + "github.com/GoCodeAlone/workflow/interfaces" +) + +// Driver implements interfaces.MigrationDriver using golang-migrate. +type Driver struct{} + +// New returns a new golang-migrate Driver. +func New() *Driver { return &Driver{} } + +// Name returns the driver name. +func (d *Driver) Name() string { return "golang-migrate" } + +// Up applies all pending migrations. +func (d *Driver) Up(ctx context.Context, req interfaces.MigrationRequest) (interfaces.MigrationResult, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationResult{}, err + } + start := time.Now() + m, err := newMigrate(req) + if err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate: %w", err) + } + defer m.Close() + + // Capture the current version before applying. + before, _, _ := m.Version() + + if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate up: %w", err) + } + + after, _, _ := m.Version() + applied := collectApplied(before, after) + + return interfaces.MigrationResult{ + Applied: applied, + DurationMs: time.Since(start).Milliseconds(), + }, nil +} + +// Down rolls back N migrations (Options.Steps, default 1). +func (d *Driver) Down(ctx context.Context, req interfaces.MigrationRequest) (interfaces.MigrationResult, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationResult{}, err + } + start := time.Now() + m, err := newMigrate(req) + if err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate: %w", err) + } + defer m.Close() + + steps := req.Options.Steps + if steps <= 0 { + steps = 1 + } + + before, _, _ := m.Version() + + if err := m.Steps(-steps); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate down: %w", err) + } + + after, _, _ := m.Version() + _ = before + _ = after + + return interfaces.MigrationResult{ + Applied: []string{fmt.Sprintf("rolled back %d migration(s)", steps)}, + DurationMs: time.Since(start).Milliseconds(), + }, nil +} + +// Status returns the current migration version and pending migrations. +func (d *Driver) Status(_ context.Context, req interfaces.MigrationRequest) (interfaces.MigrationStatus, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationStatus{}, err + } + m, err := newMigrate(req) + if err != nil { + return interfaces.MigrationStatus{}, fmt.Errorf("golang-migrate: %w", err) + } + defer m.Close() + + version, dirty, err := m.Version() + if err != nil && !errors.Is(err, migrate.ErrNilVersion) { + return interfaces.MigrationStatus{}, fmt.Errorf("golang-migrate status: %w", err) + } + + current := "" + if !errors.Is(err, migrate.ErrNilVersion) { + current = fmt.Sprintf("%d", version) + } + + return interfaces.MigrationStatus{ + Current: current, + Dirty: dirty, + }, nil +} + +// Goto migrates to the specified version (up or down). +func (d *Driver) Goto(_ context.Context, req interfaces.MigrationRequest, target string) (interfaces.MigrationResult, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationResult{}, err + } + start := time.Now() + m, err := newMigrate(req) + if err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate: %w", err) + } + defer m.Close() + + var version uint + if _, err := fmt.Sscanf(target, "%d", &version); err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate goto: invalid target version %q: %w", target, err) + } + + if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate goto: %w", err) + } + + return interfaces.MigrationResult{ + Applied: []string{target}, + DurationMs: time.Since(start).Milliseconds(), + }, nil +} + +// newMigrate creates a migrate.Migrate from a MigrationRequest. +// The DSN is expected to be a postgres:// URL; we rewrite it to pgx5:// for +// the pgx/v5 driver. +func newMigrate(req interfaces.MigrationRequest) (*migrate.Migrate, error) { + dsn := req.DSN + // golang-migrate pgx/v5 driver expects pgx5:// scheme. + if len(dsn) >= 10 && dsn[:10] == "postgres://" { + dsn = "pgx5://" + dsn[len("postgres://"):] + } else if len(dsn) >= 13 && dsn[:13] == "postgresql://" { + dsn = "pgx5://" + dsn[len("postgresql://"):] + } + + sourceURL := "file://" + req.Source.Dir + return migrate.New(sourceURL, dsn) +} + +// collectApplied returns a list of version strings between before (exclusive) +// and after (inclusive). If the versions are equal, returns empty. +func collectApplied(before, after uint) []string { + if after <= before { + return nil + } + applied := make([]string, 0, after-before) + for v := before + 1; v <= after; v++ { + applied = append(applied, fmt.Sprintf("%d", v)) + } + return applied +} diff --git a/internal/golangmigrate/driver_test.go b/internal/golangmigrate/driver_test.go new file mode 100644 index 0000000..8f9fceb --- /dev/null +++ b/internal/golangmigrate/driver_test.go @@ -0,0 +1,109 @@ +package golangmigrate_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/GoCodeAlone/workflow/interfaces" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/golangmigrate" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/testharness" +) + +func TestDriver_Name(t *testing.T) { + d := golangmigrate.New() + if got := d.Name(); got != "golang-migrate" { + t.Errorf("Name() = %q; want %q", got, "golang-migrate") + } +} + +func TestDriver_UpDownStatus(t *testing.T) { + if testing.Short() { + t.Skip("skipping: requires postgres (set up with testharness)") + } + h, err := testharness.New() + if err != nil { + t.Skipf("skipping: no postgres available: %v", err) + } + defer h.Close(t) + + dir := t.TempDir() + writeSQL(t, dir, "000001_users.up.sql", "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL);") + writeSQL(t, dir, "000001_users.down.sql", "DROP TABLE IF EXISTS users;") + writeSQL(t, dir, "000002_posts.up.sql", "CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT NOT NULL);") + writeSQL(t, dir, "000002_posts.down.sql", "DROP TABLE IF EXISTS posts;") + + ctx := context.Background() + d := golangmigrate.New() + req := interfaces.MigrationRequest{ + DSN: h.DSN(), + Source: interfaces.MigrationSource{ + Dir: dir, + }, + } + + // Status: no migrations applied yet. + st, err := d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() error: %v", err) + } + if st.Dirty { + t.Error("expected clean state before migrations") + } + + // Up: apply all. + result, err := d.Up(ctx, req) + if err != nil { + t.Fatalf("Up() error: %v", err) + } + if len(result.Applied) == 0 { + t.Error("expected at least one applied migration") + } + + // Status: current should be 2. + st, err = d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() after up error: %v", err) + } + if st.Current != "2" { + t.Errorf("Current = %q; want %q", st.Current, "2") + } + + // Down: roll back 1. + req.Options.Steps = 1 + _, err = d.Down(ctx, req) + if err != nil { + t.Fatalf("Down() error: %v", err) + } + + // Status: current should be 1. + st, err = d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() after down error: %v", err) + } + if st.Current != "1" { + t.Errorf("Current = %q; want %q", st.Current, "1") + } + + // Goto: back to 2. + _, err = d.Goto(ctx, req, "2") + if err != nil { + t.Fatalf("Goto() error: %v", err) + } + st, err = d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() after goto error: %v", err) + } + if st.Current != "2" { + t.Errorf("Goto: Current = %q; want %q", st.Current, "2") + } +} + +func writeSQL(t *testing.T, dir, name, sql string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(sql), 0o644); err != nil { + t.Fatal(err) + } +} diff --git a/internal/goose/driver.go b/internal/goose/driver.go new file mode 100644 index 0000000..90b742d --- /dev/null +++ b/internal/goose/driver.go @@ -0,0 +1,215 @@ +// Package goose provides a MigrationDriver backed by pressly/goose/v3. +package goose + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + // pgx stdlib driver for database/sql + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" + + "github.com/GoCodeAlone/workflow/interfaces" +) + +// Driver implements interfaces.MigrationDriver using pressly/goose. +type Driver struct{} + +// New returns a new goose Driver. +func New() *Driver { return &Driver{} } + +// Name returns the driver name. +func (d *Driver) Name() string { return "goose" } + +// Up applies all pending migrations. +func (d *Driver) Up(ctx context.Context, req interfaces.MigrationRequest) (interfaces.MigrationResult, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationResult{}, err + } + start := time.Now() + db, err := openDB(req.DSN) + if err != nil { + return interfaces.MigrationResult{}, err + } + defer db.Close() + + provider, err := newProvider(db, req) + if err != nil { + return interfaces.MigrationResult{}, err + } + + results, err := provider.Up(ctx) + if err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("goose up: %w", err) + } + + applied := make([]string, 0, len(results)) + for _, r := range results { + if r.Error == nil { + applied = append(applied, fmt.Sprintf("%d", r.Source.Version)) + } + } + + return interfaces.MigrationResult{ + Applied: applied, + DurationMs: time.Since(start).Milliseconds(), + }, nil +} + +// Down rolls back N migrations (Options.Steps, default 1). +func (d *Driver) Down(ctx context.Context, req interfaces.MigrationRequest) (interfaces.MigrationResult, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationResult{}, err + } + start := time.Now() + steps := req.Options.Steps + if steps <= 0 { + steps = 1 + } + + db, err := openDB(req.DSN) + if err != nil { + return interfaces.MigrationResult{}, err + } + defer db.Close() + + provider, err := newProvider(db, req) + if err != nil { + return interfaces.MigrationResult{}, err + } + + var reverted []string + for i := 0; i < steps; i++ { + res, err := provider.Down(ctx) + if err != nil { + // No more migrations to roll back. + break + } + if res != nil && res.Source != nil { + reverted = append(reverted, fmt.Sprintf("%d", res.Source.Version)) + } + } + + return interfaces.MigrationResult{ + Applied: reverted, + DurationMs: time.Since(start).Milliseconds(), + }, nil +} + +// Status returns the current migration state. +func (d *Driver) Status(_ context.Context, req interfaces.MigrationRequest) (interfaces.MigrationStatus, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationStatus{}, err + } + db, err := openDB(req.DSN) + if err != nil { + return interfaces.MigrationStatus{}, err + } + defer db.Close() + + provider, err := newProvider(db, req) + if err != nil { + return interfaces.MigrationStatus{}, err + } + + migrations, err := provider.Status(context.Background()) + if err != nil { + return interfaces.MigrationStatus{}, fmt.Errorf("goose status: %w", err) + } + + current := "" + var pending []string + for _, ms := range migrations { + switch ms.State { + case goose.StateApplied: + current = fmt.Sprintf("%d", ms.Source.Version) + case goose.StatePending: + pending = append(pending, fmt.Sprintf("%d", ms.Source.Version)) + } + } + + return interfaces.MigrationStatus{ + Current: current, + Pending: pending, + }, nil +} + +// Goto migrates to the specified version. +func (d *Driver) Goto(ctx context.Context, req interfaces.MigrationRequest, target string) (interfaces.MigrationResult, error) { + if err := req.Validate(); err != nil { + return interfaces.MigrationResult{}, err + } + start := time.Now() + + var version int64 + if _, err := fmt.Sscanf(target, "%d", &version); err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("goose goto: invalid target version %q: %w", target, err) + } + + db, err := openDB(req.DSN) + if err != nil { + return interfaces.MigrationResult{}, err + } + defer db.Close() + + provider, err := newProvider(db, req) + if err != nil { + return interfaces.MigrationResult{}, err + } + + // Determine direction by checking current status. + statuses, err := provider.Status(ctx) + if err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("goose goto: status check: %w", err) + } + + currentVersion := int64(0) + for _, ms := range statuses { + if ms.State == goose.StateApplied && ms.Source.Version > currentVersion { + currentVersion = ms.Source.Version + } + } + + var results []*goose.MigrationResult + if version >= currentVersion { + results, err = provider.UpTo(ctx, version) + } else { + results, err = provider.DownTo(ctx, version) + } + if err != nil { + return interfaces.MigrationResult{}, fmt.Errorf("goose goto %s: %w", target, err) + } + + applied := make([]string, 0, len(results)) + for _, r := range results { + if r != nil && r.Source != nil && r.Error == nil { + applied = append(applied, fmt.Sprintf("%d", r.Source.Version)) + } + } + + return interfaces.MigrationResult{ + Applied: applied, + DurationMs: time.Since(start).Milliseconds(), + }, nil +} + +func openDB(dsn string) (*sql.DB, error) { + // pgx stdlib accepts postgres:// and postgresql:// URLs. + db, err := sql.Open("pgx", dsn) + if err != nil { + return nil, fmt.Errorf("goose: open db: %w", err) + } + return db, nil +} + +func newProvider(db *sql.DB, req interfaces.MigrationRequest) (*goose.Provider, error) { + migrationsFS := os.DirFS(req.Source.Dir) + provider, err := goose.NewProvider(goose.DialectPostgres, db, migrationsFS) + if err != nil { + return nil, fmt.Errorf("goose provider: %w", err) + } + return provider, nil +} diff --git a/internal/goose/driver_test.go b/internal/goose/driver_test.go new file mode 100644 index 0000000..58944f1 --- /dev/null +++ b/internal/goose/driver_test.go @@ -0,0 +1,101 @@ +package goose_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/GoCodeAlone/workflow/interfaces" + + goosedriver "github.com/GoCodeAlone/workflow-plugin-migrations/internal/goose" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/testharness" +) + +func TestGooseDriver_Name(t *testing.T) { + d := goosedriver.New() + if got := d.Name(); got != "goose" { + t.Errorf("Name() = %q; want %q", got, "goose") + } +} + +func TestGooseDriver_UpDownStatus(t *testing.T) { + if testing.Short() { + t.Skip("skipping: requires postgres") + } + h, err := testharness.New() + if err != nil { + t.Skipf("skipping: no postgres available: %v", err) + } + defer h.Close(t) + + dir := t.TempDir() + writeGooseSQL(t, dir, "00001_users.sql", `-- +goose Up +CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL); +-- +goose Down +DROP TABLE IF EXISTS users; +`) + writeGooseSQL(t, dir, "00002_posts.sql", `-- +goose Up +CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT NOT NULL); +-- +goose Down +DROP TABLE IF EXISTS posts; +`) + + ctx := context.Background() + d := goosedriver.New() + req := interfaces.MigrationRequest{ + DSN: h.DSN(), + Source: interfaces.MigrationSource{ + Dir: dir, + }, + } + + // Status: no migrations applied. + st, err := d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() error: %v", err) + } + if st.Dirty { + t.Error("expected clean state") + } + + // Up: apply all. + result, err := d.Up(ctx, req) + if err != nil { + t.Fatalf("Up() error: %v", err) + } + if len(result.Applied) != 2 { + t.Errorf("Applied = %v; want 2 migrations", result.Applied) + } + + // Status: current = 2. + st, err = d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() after up: %v", err) + } + if st.Current != "2" { + t.Errorf("Current = %q; want 2", st.Current) + } + + // Down: roll back 1. + req.Options.Steps = 1 + _, err = d.Down(ctx, req) + if err != nil { + t.Fatalf("Down() error: %v", err) + } + + st, err = d.Status(ctx, req) + if err != nil { + t.Fatalf("Status() after down: %v", err) + } + if st.Current != "1" { + t.Errorf("Current after down = %q; want 1", st.Current) + } +} + +func writeGooseSQL(t *testing.T, dir, name, sql string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(sql), 0o644); err != nil { + t.Fatal(err) + } +} diff --git a/internal/module_driver.go b/internal/module_driver.go new file mode 100644 index 0000000..0869725 --- /dev/null +++ b/internal/module_driver.go @@ -0,0 +1,52 @@ +package internal + +import ( + "context" + "fmt" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// driverModuleConfig is the configuration for the database.migration_driver module. +type driverModuleConfig struct { + // Driver is the driver name: golang-migrate, goose, atlas. + Driver string `json:"driver"` +} + +// driverModule implements sdk.ModuleInstance for database.migration_driver. +// It declares which driver implementation this module provides. +type driverModule struct { + name string + cfg driverModuleConfig +} + +func newDriverModule(name string, rawCfg map[string]any) (sdk.ModuleInstance, error) { + cfg := driverModuleConfig{} + if v, ok := rawCfg["driver"].(string); ok { + cfg.Driver = v + } + if cfg.Driver == "" { + return nil, fmt.Errorf("database.migration_driver %q: driver is required (golang-migrate|goose|atlas)", name) + } + switch cfg.Driver { + case "golang-migrate", "goose", "atlas": + // valid + default: + return nil, fmt.Errorf("database.migration_driver %q: unknown driver %q", name, cfg.Driver) + } + return &driverModule{name: name, cfg: cfg}, nil +} + +func (m *driverModule) Init() error { return nil } +func (m *driverModule) Start(_ context.Context) error { return nil } +func (m *driverModule) Stop(_ context.Context) error { return nil } + +// InvokeMethod returns driver metadata. +func (m *driverModule) InvokeMethod(method string, _ map[string]any) (map[string]any, error) { + switch method { + case "driver_name": + return map[string]any{"driver": m.cfg.Driver}, nil + default: + return nil, fmt.Errorf("database.migration_driver %q: unknown method %q", m.name, method) + } +} diff --git a/internal/module_driver_test.go b/internal/module_driver_test.go new file mode 100644 index 0000000..31613a9 --- /dev/null +++ b/internal/module_driver_test.go @@ -0,0 +1,32 @@ +package internal + +import ( + "testing" +) + +func TestDriverModule_MissingDriver(t *testing.T) { + _, err := newDriverModule("test", map[string]any{}) + if err == nil { + t.Error("expected error for missing driver") + } +} + +func TestDriverModule_InvalidDriver(t *testing.T) { + _, err := newDriverModule("test", map[string]any{"driver": "unsupported"}) + if err == nil { + t.Error("expected error for unsupported driver") + } +} + +func TestDriverModule_ValidDrivers(t *testing.T) { + for _, name := range []string{"golang-migrate", "goose", "atlas"} { + m, err := newDriverModule("test-"+name, map[string]any{"driver": name}) + if err != nil { + t.Errorf("newDriverModule(%q): %v", name, err) + continue + } + if err := m.Init(); err != nil { + t.Errorf("Init(%q): %v", name, err) + } + } +} diff --git a/internal/module_migrations.go b/internal/module_migrations.go new file mode 100644 index 0000000..f804b06 --- /dev/null +++ b/internal/module_migrations.go @@ -0,0 +1,194 @@ +package internal + +import ( + "context" + "fmt" + "os" + "time" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/golangmigrate" + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/goose" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/driver" +) + +// migrationsModuleConfig is the configuration for the database.migrations module. +type migrationsModuleConfig struct { + // DriverRef is the name of a database.migration_driver module, OR a driver + // name (golang-migrate, goose) for inline driver configuration. + DriverRef string `json:"driver_ref"` + // Driver is the driver name when not using a module ref. + Driver string `json:"driver"` + // SourceDir is the directory containing migration files. + SourceDir string `json:"source_dir"` + // DSNEnv is the environment variable holding the database connection string. + DSNEnv string `json:"dsn_env"` + // HistoryTable is the migration history table name (driver default if empty). + HistoryTable string `json:"history_table"` + // Timeout for migration operations. Default: 5m. + Timeout string `json:"timeout"` +} + +// migrationsModule implements sdk.ModuleInstance for database.migrations. +type migrationsModule struct { + name string + cfg migrationsModuleConfig + driver driver.Driver +} + +func newMigrationsModule(name string, rawCfg map[string]any) (sdk.ModuleInstance, error) { + cfg := migrationsModuleConfig{ + Timeout: "5m", + } + if v, ok := rawCfg["driver_ref"].(string); ok { + cfg.DriverRef = v + } + if v, ok := rawCfg["driver"].(string); ok { + cfg.Driver = v + } + if v, ok := rawCfg["source_dir"].(string); ok { + cfg.SourceDir = v + } + if v, ok := rawCfg["dsn_env"].(string); ok { + cfg.DSNEnv = v + } + if v, ok := rawCfg["history_table"].(string); ok { + cfg.HistoryTable = v + } + if v, ok := rawCfg["timeout"].(string); ok { + cfg.Timeout = v + } + if cfg.SourceDir == "" { + return nil, fmt.Errorf("database.migrations %q: source_dir is required", name) + } + if cfg.DSNEnv == "" && rawCfg["dsn"] == nil { + // Allow direct dsn for test usage + if v, ok := rawCfg["dsn"].(string); !ok || v == "" { + cfg.DSNEnv = "DATABASE_URL" + } + } + return &migrationsModule{name: name, cfg: cfg}, nil +} + +func (m *migrationsModule) Init() error { + driverName := m.cfg.Driver + if driverName == "" && m.cfg.DriverRef != "" { + // When driver_ref is set to a simple name (not a module reference), + // treat it as the driver name. + driverName = m.cfg.DriverRef + } + if driverName == "" { + driverName = "golang-migrate" // sensible default + } + + switch driverName { + case "golang-migrate": + m.driver = golangmigrate.New() + case "goose": + m.driver = goose.New() + default: + return fmt.Errorf("database.migrations %q: unknown driver %q", m.name, driverName) + } + return nil +} + +func (m *migrationsModule) Start(_ context.Context) error { return nil } +func (m *migrationsModule) Stop(_ context.Context) error { return nil } + +// InvokeMethod dispatches up/down/status/goto calls to the driver. +func (m *migrationsModule) InvokeMethod(method string, args map[string]any) (map[string]any, error) { + dsn := m.resolveDSN(args) + if dsn == "" { + return nil, fmt.Errorf("database.migrations %q: no DSN available", m.name) + } + sourceDir := m.cfg.SourceDir + if v, ok := args["source_dir"].(string); ok && v != "" { + sourceDir = v + } + + timeout := 5 * time.Minute + if t, err := time.ParseDuration(m.cfg.Timeout); err == nil { + timeout = t + } + + req := driver.Request{ + DSN: dsn, + Source: driver.Source{ + Dir: sourceDir, + }, + Options: driver.Options{ + Timeout: timeout, + }, + } + + ctx := context.Background() + + switch method { + case "up": + result, err := m.driver.Up(ctx, req) + if err != nil { + return nil, err + } + return map[string]any{ + "applied": result.Applied, + "skipped": result.Skipped, + "duration_ms": result.DurationMs, + }, nil + case "down": + steps := 1 + if v, ok := args["steps"].(int); ok { + steps = v + } + req.Options.Steps = steps + result, err := m.driver.Down(ctx, req) + if err != nil { + return nil, err + } + return map[string]any{ + "applied": result.Applied, + "skipped": result.Skipped, + "duration_ms": result.DurationMs, + }, nil + case "status": + status, err := m.driver.Status(ctx, req) + if err != nil { + return nil, err + } + return map[string]any{ + "current": status.Current, + "pending": status.Pending, + "dirty": status.Dirty, + }, nil + case "goto": + target, _ := args["target"].(string) + if target == "" { + return nil, fmt.Errorf("goto: target version is required") + } + result, err := m.driver.Goto(ctx, req, target) + if err != nil { + return nil, err + } + return map[string]any{ + "applied": result.Applied, + "skipped": result.Skipped, + "duration_ms": result.DurationMs, + }, nil + default: + return nil, fmt.Errorf("database.migrations %q: unknown method %q", m.name, method) + } +} + +func (m *migrationsModule) resolveDSN(args map[string]any) string { + // 1. Arg override + if v, ok := args["dsn"].(string); ok && v != "" { + return v + } + // 2. Module config DSN env + if m.cfg.DSNEnv != "" { + if v := os.Getenv(m.cfg.DSNEnv); v != "" { + return v + } + } + return "" +} diff --git a/internal/module_migrations_test.go b/internal/module_migrations_test.go new file mode 100644 index 0000000..2b384e1 --- /dev/null +++ b/internal/module_migrations_test.go @@ -0,0 +1,75 @@ +package internal + +import ( + "context" + "testing" +) + +func TestMigrationsModule_Init_MissingSourceDir(t *testing.T) { + _, err := newMigrationsModule("test", map[string]any{ + "driver": "golang-migrate", + // source_dir deliberately omitted + }) + if err == nil { + t.Error("expected error for missing source_dir") + } +} + +func TestMigrationsModule_Init_UnknownDriver(t *testing.T) { + m, err := newMigrationsModule("test", map[string]any{ + "driver": "unknown-driver", + "source_dir": "/tmp/migrations", + }) + if err != nil { + t.Fatalf("newMigrationsModule: %v", err) + } + if err := m.Init(); err == nil { + t.Error("expected error for unknown driver") + } +} + +func TestMigrationsModule_Init_GolangMigrate(t *testing.T) { + m, err := newMigrationsModule("test", map[string]any{ + "driver": "golang-migrate", + "source_dir": "/tmp/migrations", + }) + if err != nil { + t.Fatalf("newMigrationsModule: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init() with golang-migrate: %v", err) + } +} + +func TestMigrationsModule_Init_Goose(t *testing.T) { + m, err := newMigrationsModule("test", map[string]any{ + "driver": "goose", + "source_dir": "/tmp/migrations", + }) + if err != nil { + t.Fatalf("newMigrationsModule: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init() with goose: %v", err) + } +} + +func TestMigrationsModule_StartStop(t *testing.T) { + m, err := newMigrationsModule("test", map[string]any{ + "driver": "golang-migrate", + "source_dir": "/tmp/migrations", + }) + if err != nil { + t.Fatalf("newMigrationsModule: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init(): %v", err) + } + ctx := context.Background() + if err := m.Start(ctx); err != nil { + t.Fatalf("Start(): %v", err) + } + if err := m.Stop(ctx); err != nil { + t.Fatalf("Stop(): %v", err) + } +} diff --git a/internal/plugin.go b/internal/plugin.go new file mode 100644 index 0000000..a9f8505 --- /dev/null +++ b/internal/plugin.go @@ -0,0 +1,80 @@ +// Package internal implements the workflow-plugin-migrations plugin. +// It provides database.migrations and database.migration_driver module types, +// plus step.migrate_up, step.migrate_down, step.migrate_status, step.migrate_to +// step types. Drivers: golang-migrate, goose. +package internal + +import ( + "fmt" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/steps" +) + +// Version is set at build time via -ldflags. +var Version = "0.0.0" + +// MigrationsPlugin implements sdk.PluginProvider, sdk.ModuleProvider, and sdk.StepProvider. +type MigrationsPlugin struct{} + +// NewPlugin returns a new plugin instance. +func NewPlugin() sdk.PluginProvider { + return &MigrationsPlugin{} +} + +// Manifest returns plugin metadata. +func (p *MigrationsPlugin) Manifest() sdk.PluginManifest { + return sdk.PluginManifest{ + Name: "workflow-plugin-migrations", + Version: Version, + Author: "GoCodeAlone", + Description: "Database migration plugin: golang-migrate + goose drivers, pre-deploy runner, wfctl migrate CLI", + } +} + +// ModuleTypes returns module type names this plugin provides. +func (p *MigrationsPlugin) ModuleTypes() []string { + return []string{ + "database.migrations", + "database.migration_driver", + } +} + +// CreateModule creates a module instance of the given type. +func (p *MigrationsPlugin) CreateModule(typeName, name string, cfg map[string]any) (sdk.ModuleInstance, error) { + switch typeName { + case "database.migrations": + return newMigrationsModule(name, cfg) + case "database.migration_driver": + return newDriverModule(name, cfg) + default: + return nil, fmt.Errorf("workflow-plugin-migrations: unknown module type %q", typeName) + } +} + +// StepTypes returns step type names this plugin provides. +func (p *MigrationsPlugin) StepTypes() []string { + return []string{ + "step.migrate_up", + "step.migrate_down", + "step.migrate_status", + "step.migrate_to", + } +} + +// CreateStep creates a step instance of the given type. +func (p *MigrationsPlugin) CreateStep(typeName, name string, cfg map[string]any) (sdk.StepInstance, error) { + switch typeName { + case "step.migrate_up": + return steps.NewMigrateUpStep(name, cfg), nil + case "step.migrate_down": + return steps.NewMigrateDownStep(name, cfg), nil + case "step.migrate_status": + return steps.NewMigrateStatusStep(name, cfg), nil + case "step.migrate_to": + return steps.NewMigrateToStep(name, cfg), nil + default: + return nil, fmt.Errorf("workflow-plugin-migrations: unknown step type %q", typeName) + } +} diff --git a/internal/plugin_test.go b/internal/plugin_test.go new file mode 100644 index 0000000..df0e90f --- /dev/null +++ b/internal/plugin_test.go @@ -0,0 +1,114 @@ +package internal_test + +import ( + "testing" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal" +) + +func TestPlugin_Manifest(t *testing.T) { + p := internal.NewPlugin() + m := p.Manifest() + if m.Name == "" { + t.Error("Manifest.Name is empty") + } + if m.Version == "" { + t.Error("Manifest.Version is empty") + } +} + +func TestPlugin_ModuleTypes(t *testing.T) { + p, ok := internal.NewPlugin().(sdk.ModuleProvider) + if !ok { + t.Fatal("plugin does not implement sdk.ModuleProvider") + } + types := p.ModuleTypes() + want := map[string]bool{ + "database.migrations": true, + "database.migration_driver": true, + } + for _, tp := range types { + delete(want, tp) + } + if len(want) > 0 { + t.Errorf("missing module types: %v", want) + } +} + +func TestPlugin_StepTypes(t *testing.T) { + p, ok := internal.NewPlugin().(sdk.StepProvider) + if !ok { + t.Fatal("plugin does not implement sdk.StepProvider") + } + types := p.StepTypes() + want := map[string]bool{ + "step.migrate_up": true, + "step.migrate_down": true, + "step.migrate_status": true, + "step.migrate_to": true, + } + for _, tp := range types { + delete(want, tp) + } + if len(want) > 0 { + t.Errorf("missing step types: %v", want) + } +} + +func TestPlugin_CreateModule_Unknown(t *testing.T) { + p, ok := internal.NewPlugin().(sdk.ModuleProvider) + if !ok { + t.Fatal("plugin does not implement sdk.ModuleProvider") + } + _, err := p.CreateModule("unknown.type", "x", nil) + if err == nil { + t.Error("expected error for unknown module type") + } +} + +func TestPlugin_CreateStep_Unknown(t *testing.T) { + p, ok := internal.NewPlugin().(sdk.StepProvider) + if !ok { + t.Fatal("plugin does not implement sdk.StepProvider") + } + _, err := p.CreateStep("step.unknown", "x", nil) + if err == nil { + t.Error("expected error for unknown step type") + } +} + +func TestPlugin_CreateStep_MigrateUp(t *testing.T) { + p, ok := internal.NewPlugin().(sdk.StepProvider) + if !ok { + t.Fatal("plugin does not implement sdk.StepProvider") + } + step, err := p.CreateStep("step.migrate_up", "my-step", map[string]any{ + "driver": "golang-migrate", + "source_dir": "/tmp/migrations", + }) + if err != nil { + t.Fatalf("CreateStep(step.migrate_up): %v", err) + } + if step == nil { + t.Error("CreateStep returned nil") + } +} + +func TestPlugin_CreateModule_MigrationsValidConfig(t *testing.T) { + p, ok := internal.NewPlugin().(sdk.ModuleProvider) + if !ok { + t.Fatal("plugin does not implement sdk.ModuleProvider") + } + m, err := p.CreateModule("database.migrations", "my-db", map[string]any{ + "driver": "golang-migrate", + "source_dir": "/tmp/migrations", + }) + if err != nil { + t.Fatalf("CreateModule(database.migrations): %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init(): %v", err) + } +} diff --git a/internal/steps/base.go b/internal/steps/base.go new file mode 100644 index 0000000..b00820f --- /dev/null +++ b/internal/steps/base.go @@ -0,0 +1,263 @@ +// Package steps provides pipeline step implementations for database migrations. +package steps + +import ( + "context" + "fmt" + "os" + "time" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/golangmigrate" + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/goose" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/driver" +) + +// stepConfig holds the shared configuration for all migrate steps. +type stepConfig struct { + // Driver to use: golang-migrate (default) or goose. + Driver string `json:"driver"` + // SourceDir is the directory containing migration files. + SourceDir string `json:"source_dir"` + // DSNEnv is the environment variable holding the database connection string. + DSNEnv string `json:"dsn_env"` + // DSN is a literal DSN (overrides DSNEnv; useful in tests). + DSN string `json:"dsn"` + // Timeout for the migration operation. Default: 5m. + Timeout string `json:"timeout"` +} + +// parseStepConfig parses a raw config map into a stepConfig. +func parseStepConfig(raw map[string]any) stepConfig { + cfg := stepConfig{ + Driver: "golang-migrate", + DSNEnv: "DATABASE_URL", + Timeout: "5m", + } + if v, ok := raw["driver"].(string); ok && v != "" { + cfg.Driver = v + } + if v, ok := raw["source_dir"].(string); ok { + cfg.SourceDir = v + } + if v, ok := raw["dsn_env"].(string); ok && v != "" { + cfg.DSNEnv = v + } + if v, ok := raw["dsn"].(string); ok { + cfg.DSN = v + } + if v, ok := raw["timeout"].(string); ok && v != "" { + cfg.Timeout = v + } + return cfg +} + +// resolveDSN returns the DSN from the step config, falling back to env var. +func (c stepConfig) resolveDSN() string { + if c.DSN != "" { + return c.DSN + } + return os.Getenv(c.DSNEnv) +} + +// buildDriver instantiates the named migration driver. +func buildDriver(name string) (driver.Driver, error) { + switch name { + case "golang-migrate", "": + return golangmigrate.New(), nil + case "goose": + return goose.New(), nil + default: + return nil, fmt.Errorf("unknown driver %q", name) + } +} + +// buildRequest constructs a MigrationRequest from a stepConfig. +func buildRequest(cfg stepConfig) (driver.Request, time.Duration, error) { + dsn := cfg.resolveDSN() + if dsn == "" { + return driver.Request{}, 0, fmt.Errorf("no DSN available (set %s env var or dsn config)", cfg.DSNEnv) + } + if cfg.SourceDir == "" { + return driver.Request{}, 0, fmt.Errorf("source_dir is required") + } + timeout := 5 * time.Minute + if t, err := time.ParseDuration(cfg.Timeout); err == nil { + timeout = t + } + req := driver.Request{ + DSN: dsn, + Source: driver.Source{ + Dir: cfg.SourceDir, + }, + Options: driver.Options{ + Timeout: timeout, + }, + } + return req, timeout, nil +} + +// resultToOutput converts a MigrationResult to a step output map. +func resultToOutput(r driver.Result) map[string]any { + return map[string]any{ + "applied": r.Applied, + "skipped": r.Skipped, + "duration_ms": r.DurationMs, + } +} + +// MigrateUpStep executes all pending migrations (up). +type MigrateUpStep struct { + name string + cfg stepConfig +} + +// NewMigrateUpStep returns a new step.migrate_up step. +func NewMigrateUpStep(name string, raw map[string]any) sdk.StepInstance { + return &MigrateUpStep{name: name, cfg: parseStepConfig(raw)} +} + +// Execute applies all pending migrations. +func (s *MigrateUpStep) Execute(ctx context.Context, _ map[string]any, _ map[string]map[string]any, _ map[string]any, _ map[string]any, raw map[string]any) (*sdk.StepResult, error) { + cfg := s.cfg + // Allow runtime overrides via step config pass-through. + if raw != nil { + merged := parseStepConfig(raw) + if merged.SourceDir != "" { + cfg.SourceDir = merged.SourceDir + } + if merged.DSN != "" { + cfg.DSN = merged.DSN + } + } + + d, err := buildDriver(cfg.Driver) + if err != nil { + return nil, fmt.Errorf("step.migrate_up %s: %w", s.name, err) + } + req, timeout, err := buildRequest(cfg) + if err != nil { + return nil, fmt.Errorf("step.migrate_up %s: %w", s.name, err) + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + result, err := d.Up(ctx, req) + if err != nil { + return nil, fmt.Errorf("step.migrate_up %s: %w", s.name, err) + } + return &sdk.StepResult{Output: resultToOutput(result)}, nil +} + +// MigrateDownStep rolls back N migrations (down). +type MigrateDownStep struct { + name string + cfg stepConfig + steps int +} + +// NewMigrateDownStep returns a new step.migrate_down step. +func NewMigrateDownStep(name string, raw map[string]any) sdk.StepInstance { + steps := 1 + if v, ok := raw["steps"].(int); ok && v > 0 { + steps = v + } + return &MigrateDownStep{name: name, cfg: parseStepConfig(raw), steps: steps} +} + +// Execute rolls back N migrations. +func (s *MigrateDownStep) Execute(ctx context.Context, _ map[string]any, _ map[string]map[string]any, _ map[string]any, _ map[string]any, _ map[string]any) (*sdk.StepResult, error) { + d, err := buildDriver(s.cfg.Driver) + if err != nil { + return nil, fmt.Errorf("step.migrate_down %s: %w", s.name, err) + } + req, timeout, err := buildRequest(s.cfg) + if err != nil { + return nil, fmt.Errorf("step.migrate_down %s: %w", s.name, err) + } + req.Options.Steps = s.steps + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + result, err := d.Down(ctx, req) + if err != nil { + return nil, fmt.Errorf("step.migrate_down %s: %w", s.name, err) + } + return &sdk.StepResult{Output: resultToOutput(result)}, nil +} + +// MigrateStatusStep returns the current migration status. +type MigrateStatusStep struct { + name string + cfg stepConfig +} + +// NewMigrateStatusStep returns a new step.migrate_status step. +func NewMigrateStatusStep(name string, raw map[string]any) sdk.StepInstance { + return &MigrateStatusStep{name: name, cfg: parseStepConfig(raw)} +} + +// Execute returns the migration status. +func (s *MigrateStatusStep) Execute(ctx context.Context, _ map[string]any, _ map[string]map[string]any, _ map[string]any, _ map[string]any, _ map[string]any) (*sdk.StepResult, error) { + d, err := buildDriver(s.cfg.Driver) + if err != nil { + return nil, fmt.Errorf("step.migrate_status %s: %w", s.name, err) + } + req, timeout, err := buildRequest(s.cfg) + if err != nil { + return nil, fmt.Errorf("step.migrate_status %s: %w", s.name, err) + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + status, err := d.Status(ctx, req) + if err != nil { + return nil, fmt.Errorf("step.migrate_status %s: %w", s.name, err) + } + return &sdk.StepResult{Output: map[string]any{ + "current": status.Current, + "pending": status.Pending, + "dirty": status.Dirty, + }}, nil +} + +// MigrateToStep migrates to a specific version. +type MigrateToStep struct { + name string + cfg stepConfig + target string +} + +// NewMigrateToStep returns a new step.migrate_to step. +func NewMigrateToStep(name string, raw map[string]any) sdk.StepInstance { + target, _ := raw["target"].(string) + return &MigrateToStep{name: name, cfg: parseStepConfig(raw), target: target} +} + +// Execute migrates the database to the given target version. +func (s *MigrateToStep) Execute(ctx context.Context, _ map[string]any, _ map[string]map[string]any, _ map[string]any, _ map[string]any, _ map[string]any) (*sdk.StepResult, error) { + if s.target == "" { + return nil, fmt.Errorf("step.migrate_to %s: target version is required", s.name) + } + d, err := buildDriver(s.cfg.Driver) + if err != nil { + return nil, fmt.Errorf("step.migrate_to %s: %w", s.name, err) + } + req, timeout, err := buildRequest(s.cfg) + if err != nil { + return nil, fmt.Errorf("step.migrate_to %s: %w", s.name, err) + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + result, err := d.Goto(ctx, req, s.target) + if err != nil { + return nil, fmt.Errorf("step.migrate_to %s: %w", s.name, err) + } + return &sdk.StepResult{Output: resultToOutput(result)}, nil +} diff --git a/internal/steps/base_test.go b/internal/steps/base_test.go new file mode 100644 index 0000000..a6e3959 --- /dev/null +++ b/internal/steps/base_test.go @@ -0,0 +1,72 @@ +package steps_test + +import ( + "context" + "testing" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/steps" +) + +func TestNewMigrateUpStep_Creation(t *testing.T) { + s := steps.NewMigrateUpStep("my-up", map[string]any{ + "driver": "golang-migrate", + "source_dir": "/tmp/migrations", + "dsn": "postgres://localhost/test", + }) + if s == nil { + t.Error("NewMigrateUpStep returned nil") + } +} + +func TestNewMigrateDownStep_Creation(t *testing.T) { + s := steps.NewMigrateDownStep("my-down", map[string]any{ + "driver": "goose", + "source_dir": "/tmp/migrations", + "steps": 2, + }) + if s == nil { + t.Error("NewMigrateDownStep returned nil") + } +} + +func TestNewMigrateStatusStep_Creation(t *testing.T) { + s := steps.NewMigrateStatusStep("my-status", map[string]any{ + "source_dir": "/tmp/migrations", + }) + if s == nil { + t.Error("NewMigrateStatusStep returned nil") + } +} + +func TestNewMigrateToStep_Creation(t *testing.T) { + s := steps.NewMigrateToStep("my-to", map[string]any{ + "source_dir": "/tmp/migrations", + "target": "5", + }) + if s == nil { + t.Error("NewMigrateToStep returned nil") + } +} + +func TestMigrateToStep_MissingTarget(t *testing.T) { + s := steps.NewMigrateToStep("my-to", map[string]any{ + "source_dir": "/tmp/migrations", + // target deliberately omitted + }) + // Execute without target should fail. + _, err := s.Execute(context.Background(), nil, nil, nil, nil, nil) + if err == nil { + t.Error("expected error when target is missing") + } +} + +func TestMigrateUpStep_MissingSourceDir(t *testing.T) { + s := steps.NewMigrateUpStep("my-up", map[string]any{ + "dsn": "postgres://localhost/test", + // source_dir omitted + }) + _, err := s.Execute(context.Background(), nil, nil, nil, nil, nil) + if err == nil { + t.Error("expected error when source_dir is missing") + } +} diff --git a/pkg/cli/root.go b/pkg/cli/root.go new file mode 100644 index 0000000..79d1004 --- /dev/null +++ b/pkg/cli/root.go @@ -0,0 +1,192 @@ +// Package cli provides the shared Cobra root command for `wfctl migrate *` +// and the standalone `workflow-migrate` binary. Both entry points call the +// same command tree so the behaviour is identical regardless of invocation. +package cli + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/GoCodeAlone/workflow/interfaces" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/golangmigrate" + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/goose" +) + +// cliProvider implements sdk.CLIProvider by dispatching to the Cobra root. +type cliProvider struct{} + +// NewCLIProvider returns a new CLIProvider. +func NewCLIProvider() *cliProvider { return &cliProvider{} } + +// RunCLI implements sdk.CLIProvider. +func (c *cliProvider) RunCLI(args []string) int { + root := NewRoot() + root.SetArgs(args) + if err := root.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + return 0 +} + +// NewRoot builds the Cobra root command for the migrate CLI. +func NewRoot() *cobra.Command { + root := &cobra.Command{ + Use: "migrate", + Short: "Database migration commands", + Long: "Run, inspect, and test database migrations via golang-migrate or goose.", + } + root.AddCommand( + newUpCmd(), + newDownCmd(), + newStatusCmd(), + newGotoCmd(), + ) + return root +} + +// sharedFlags adds the common driver/DSN/source-dir flags to a command. +func sharedFlags(cmd *cobra.Command) { + cmd.Flags().String("driver", "golang-migrate", "Migration driver (golang-migrate|goose)") + cmd.Flags().String("source-dir", "", "Directory containing migration files (required)") + cmd.Flags().String("dsn", "", "Database connection string (overrides DATABASE_URL env var)") + _ = cmd.MarkFlagRequired("source-dir") +} + +// buildDriverAndRequest resolves the driver and constructs a MigrationRequest from flags. +func buildDriverAndRequest(cmd *cobra.Command) (interfaces.MigrationDriver, interfaces.MigrationRequest, error) { + driverName, _ := cmd.Flags().GetString("driver") + sourceDir, _ := cmd.Flags().GetString("source-dir") + dsn, _ := cmd.Flags().GetString("dsn") + if dsn == "" { + dsn = os.Getenv("DATABASE_URL") + } + if dsn == "" { + return nil, interfaces.MigrationRequest{}, fmt.Errorf("no DSN: set --dsn or DATABASE_URL env var") + } + + var d interfaces.MigrationDriver + switch driverName { + case "golang-migrate", "": + d = golangmigrate.New() + case "goose": + d = goose.New() + default: + return nil, interfaces.MigrationRequest{}, fmt.Errorf("unknown driver %q (supported: golang-migrate, goose)", driverName) + } + + req := interfaces.MigrationRequest{ + DSN: dsn, + Source: interfaces.MigrationSource{Dir: sourceDir}, + } + return d, req, nil +} + +func newUpCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "up", + Short: "Apply all pending migrations", + RunE: func(cmd *cobra.Command, _ []string) error { + d, req, err := buildDriverAndRequest(cmd) + if err != nil { + return err + } + result, err := d.Up(context.Background(), req) + if err != nil { + return fmt.Errorf("migrate up: %w", err) + } + if len(result.Applied) == 0 { + fmt.Println("No pending migrations.") + return nil + } + fmt.Printf("Applied %d migration(s): %v\n", len(result.Applied), result.Applied) + return nil + }, + } + sharedFlags(cmd) + return cmd +} + +func newDownCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "down", + Short: "Roll back N migrations (default: 1)", + RunE: func(cmd *cobra.Command, _ []string) error { + d, req, err := buildDriverAndRequest(cmd) + if err != nil { + return err + } + steps, _ := cmd.Flags().GetInt("steps") + req.Options.Steps = steps + result, err := d.Down(context.Background(), req) + if err != nil { + return fmt.Errorf("migrate down: %w", err) + } + fmt.Printf("Rolled back %d migration(s): %v\n", len(result.Applied), result.Applied) + return nil + }, + } + sharedFlags(cmd) + cmd.Flags().Int("steps", 1, "Number of migrations to roll back") + return cmd +} + +func newStatusCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Show current migration status", + RunE: func(cmd *cobra.Command, _ []string) error { + d, req, err := buildDriverAndRequest(cmd) + if err != nil { + return err + } + st, err := d.Status(context.Background(), req) + if err != nil { + return fmt.Errorf("migrate status: %w", err) + } + if st.Current == "" { + fmt.Println("No migrations applied.") + } else { + fmt.Printf("Current: %s\n", st.Current) + } + if len(st.Pending) > 0 { + fmt.Printf("Pending: %v\n", st.Pending) + } else { + fmt.Println("No pending migrations.") + } + if st.Dirty { + fmt.Println("WARNING: database is in dirty state!") + } + return nil + }, + } + sharedFlags(cmd) + return cmd +} + +func newGotoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "goto ", + Short: "Migrate to a specific version (up or down)", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + d, req, err := buildDriverAndRequest(cmd) + if err != nil { + return err + } + target := args[0] + result, err := d.Goto(context.Background(), req, target) + if err != nil { + return fmt.Errorf("migrate goto %s: %w", target, err) + } + fmt.Printf("Migrated to %s (%d steps): %v\n", target, len(result.Applied), result.Applied) + return nil + }, + } + sharedFlags(cmd) + return cmd +} diff --git a/pkg/conformance/corpus/001_create_users.down.sql b/pkg/conformance/corpus/001_create_users.down.sql new file mode 100644 index 0000000..c99ddcd --- /dev/null +++ b/pkg/conformance/corpus/001_create_users.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; diff --git a/pkg/conformance/corpus/001_create_users.up.sql b/pkg/conformance/corpus/001_create_users.up.sql new file mode 100644 index 0000000..97deef9 --- /dev/null +++ b/pkg/conformance/corpus/001_create_users.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL +); diff --git a/pkg/conformance/corpus/002_add_indexes.down.sql b/pkg/conformance/corpus/002_add_indexes.down.sql new file mode 100644 index 0000000..eb8bead --- /dev/null +++ b/pkg/conformance/corpus/002_add_indexes.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS idx_users_email; +DROP INDEX IF EXISTS idx_users_name; diff --git a/pkg/conformance/corpus/002_add_indexes.up.sql b/pkg/conformance/corpus/002_add_indexes.up.sql new file mode 100644 index 0000000..885bd98 --- /dev/null +++ b/pkg/conformance/corpus/002_add_indexes.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email); +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_name ON users (name); diff --git a/pkg/conformance/suite.go b/pkg/conformance/suite.go new file mode 100644 index 0000000..fd0eb71 --- /dev/null +++ b/pkg/conformance/suite.go @@ -0,0 +1,212 @@ +// Package conformance provides a test suite that every MigrationDriver must pass. +// Third-party driver authors import this package and call Suite.Run(t, driver) to +// verify their implementation against the standard behavioural matrix. +package conformance + +import ( + "context" + "embed" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/GoCodeAlone/workflow/interfaces" +) + +//go:embed corpus +var corpusFS embed.FS + +// Suite runs the conformance test matrix against a given MigrationDriver. +type Suite struct { + // DSN is the PostgreSQL connection string to use. + DSN string + // SourceDir is overridden per sub-test with a temp dir containing the corpus. + sourceDir string +} + +// NewSuite creates a Suite using the provided DSN. +func NewSuite(dsn string) *Suite { + return &Suite{DSN: dsn} +} + +// Run executes all conformance cases against the given driver. +func (s *Suite) Run(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + dir := s.extractCorpus(t) + s.sourceDir = dir + + t.Run("FreshUpAll", func(t *testing.T) { s.testFreshUpAll(t, d) }) + t.Run("IdempotentUp", func(t *testing.T) { s.testIdempotentUp(t, d) }) + t.Run("DownOne", func(t *testing.T) { s.testDownOne(t, d) }) + t.Run("DownAll", func(t *testing.T) { s.testDownAll(t, d) }) + t.Run("GotoVersion", func(t *testing.T) { s.testGotoVersion(t, d) }) + t.Run("StatusReflectsState", func(t *testing.T) { s.testStatusReflectsState(t, d) }) +} + +// req builds a MigrationRequest for the suite's DSN and source dir. +func (s *Suite) req() interfaces.MigrationRequest { + return interfaces.MigrationRequest{ + DSN: s.DSN, + Source: interfaces.MigrationSource{Dir: s.sourceDir}, + } +} + +// testFreshUpAll asserts that Up applies all migrations on a fresh DB. +func (s *Suite) testFreshUpAll(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + ctx := context.Background() + result, err := d.Up(ctx, s.req()) + if err != nil { + t.Fatalf("Up() error: %v", err) + } + if len(result.Applied) == 0 { + t.Error("expected at least 1 applied migration") + } + + st, err := d.Status(ctx, s.req()) + if err != nil { + t.Fatalf("Status() error: %v", err) + } + if st.Dirty { + t.Error("unexpected dirty state after Up") + } + if len(st.Pending) > 0 { + t.Errorf("unexpected pending migrations after Up: %v", st.Pending) + } +} + +// testIdempotentUp asserts that running Up again is a no-op. +func (s *Suite) testIdempotentUp(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + ctx := context.Background() + + // Apply all first. + if _, err := d.Up(ctx, s.req()); err != nil { + t.Fatalf("initial Up() error: %v", err) + } + + // Re-run — should succeed with no new applied migrations. + result, err := d.Up(ctx, s.req()) + if err != nil { + t.Fatalf("idempotent Up() error: %v", err) + } + if len(result.Applied) > 0 { + t.Errorf("idempotent Up applied %v; want none", result.Applied) + } +} + +// testDownOne asserts that Down rolls back exactly one migration. +func (s *Suite) testDownOne(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + ctx := context.Background() + + if _, err := d.Up(ctx, s.req()); err != nil { + t.Fatalf("Up() error: %v", err) + } + + stBefore, _ := d.Status(ctx, s.req()) + + req := s.req() + req.Options.Steps = 1 + if _, err := d.Down(ctx, req); err != nil { + t.Fatalf("Down(1) error: %v", err) + } + + stAfter, err := d.Status(ctx, s.req()) + if err != nil { + t.Fatalf("Status() after Down error: %v", err) + } + if stAfter.Current == stBefore.Current { + t.Error("Down did not change current version") + } +} + +// testDownAll asserts that repeated Down reaches an empty state. +func (s *Suite) testDownAll(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + ctx := context.Background() + + if _, err := d.Up(ctx, s.req()); err != nil { + t.Fatalf("Up() error: %v", err) + } + + // Roll back all migrations one at a time. + for i := 0; i < 10; i++ { + req := s.req() + req.Options.Steps = 1 + _, err := d.Down(ctx, req) + if err != nil { + // No more migrations to roll back. + break + } + } + + st, err := d.Status(ctx, s.req()) + if err != nil { + t.Fatalf("Status() after DownAll error: %v", err) + } + if st.Current != "" && st.Current != "0" { + t.Errorf("after DownAll current = %q; want empty or 0", st.Current) + } +} + +// testGotoVersion asserts that Goto moves the DB to the target version. +func (s *Suite) testGotoVersion(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + ctx := context.Background() + + if _, err := d.Up(ctx, s.req()); err != nil { + t.Fatalf("Up() error: %v", err) + } + + // Go to version 1. + if _, err := d.Goto(ctx, s.req(), "1"); err != nil { + t.Fatalf("Goto(1) error: %v", err) + } + + st, err := d.Status(ctx, s.req()) + if err != nil { + t.Fatalf("Status() after Goto error: %v", err) + } + if st.Current != "1" { + t.Errorf("Goto(1): current = %q; want 1", st.Current) + } +} + +// testStatusReflectsState asserts that Status correctly reflects pending migrations. +func (s *Suite) testStatusReflectsState(t *testing.T, d interfaces.MigrationDriver) { + t.Helper() + ctx := context.Background() + + st, err := d.Status(ctx, s.req()) + if err != nil { + t.Fatalf("Status() error: %v", err) + } + // On a fresh DB all migrations should be pending. + if len(st.Pending) == 0 { + t.Error("expected pending migrations on fresh DB") + } +} + +// extractCorpus copies embedded corpus SQL files to a temporary directory and +// returns its path. +func (s *Suite) extractCorpus(t *testing.T) string { + t.Helper() + dir := t.TempDir() + err := fs.WalkDir(corpusFS, "corpus", func(path string, de fs.DirEntry, err error) error { + if err != nil || de.IsDir() { + return err + } + data, err := corpusFS.ReadFile(path) + if err != nil { + return err + } + dest := filepath.Join(dir, filepath.Base(path)) + return os.WriteFile(dest, data, 0o644) + }) + if err != nil { + t.Fatalf("extract corpus: %v", err) + } + return dir +} diff --git a/pkg/conformance/suite_test.go b/pkg/conformance/suite_test.go new file mode 100644 index 0000000..3be6a90 --- /dev/null +++ b/pkg/conformance/suite_test.go @@ -0,0 +1,38 @@ +package conformance_test + +import ( + "testing" + + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/golangmigrate" + "github.com/GoCodeAlone/workflow-plugin-migrations/internal/goose" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/conformance" + "github.com/GoCodeAlone/workflow-plugin-migrations/pkg/testharness" +) + +func TestConformance_GolangMigrate(t *testing.T) { + if testing.Short() { + t.Skip("skipping conformance: requires postgres") + } + h, err := testharness.New() + if err != nil { + t.Skipf("no postgres available: %v", err) + } + defer h.Close(t) + + suite := conformance.NewSuite(h.DSN()) + suite.Run(t, golangmigrate.New()) +} + +func TestConformance_Goose(t *testing.T) { + if testing.Short() { + t.Skip("skipping conformance: requires postgres") + } + h, err := testharness.New() + if err != nil { + t.Skipf("no postgres available: %v", err) + } + defer h.Close(t) + + suite := conformance.NewSuite(h.DSN()) + suite.Run(t, goose.New()) +} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go new file mode 100644 index 0000000..d963792 --- /dev/null +++ b/pkg/driver/driver.go @@ -0,0 +1,60 @@ +// Package driver provides the MigrationDriver adapter and in-process registry. +package driver + +import ( + "context" + + "github.com/GoCodeAlone/workflow/interfaces" +) + +// Driver is a thin alias so callers can import from this package +// without directly depending on the interfaces package. +type Driver = interfaces.MigrationDriver + +// Request is an alias for interfaces.MigrationRequest. +type Request = interfaces.MigrationRequest + +// Source is an alias for interfaces.MigrationSource. +type Source = interfaces.MigrationSource + +// Options is an alias for interfaces.MigrationOptions. +type Options = interfaces.MigrationOptions + +// Result is an alias for interfaces.MigrationResult. +type Result = interfaces.MigrationResult + +// Status is an alias for interfaces.MigrationStatus. +type Status = interfaces.MigrationStatus + +// Adapter wraps any MigrationDriver and adds optional pre/post hooks. +type Adapter struct { + inner Driver +} + +// NewAdapter wraps a MigrationDriver in an Adapter. +func NewAdapter(d Driver) *Adapter { + return &Adapter{inner: d} +} + +// Name delegates to the inner driver. +func (a *Adapter) Name() string { return a.inner.Name() } + +// Up delegates to the inner driver. +func (a *Adapter) Up(ctx context.Context, req Request) (Result, error) { + return a.inner.Up(ctx, req) +} + +// Down delegates to the inner driver. +func (a *Adapter) Down(ctx context.Context, req Request) (Result, error) { + return a.inner.Down(ctx, req) +} + +// Status delegates to the inner driver. +func (a *Adapter) Status(ctx context.Context, req Request) (Status, error) { + return a.inner.Status(ctx, req) +} + +// Goto delegates to the inner driver. +func (a *Adapter) Goto(ctx context.Context, req Request, target string) (Result, error) { + return a.inner.Goto(ctx, req, target) +} diff --git a/pkg/driver/registry.go b/pkg/driver/registry.go new file mode 100644 index 0000000..e7ae26f --- /dev/null +++ b/pkg/driver/registry.go @@ -0,0 +1,63 @@ +package driver + +import ( + "fmt" + "sync" +) + +// Registry is a thread-safe in-process registry of named MigrationDrivers. +type Registry struct { + mu sync.RWMutex + drivers map[string]Driver +} + +// NewRegistry returns an empty Registry. +func NewRegistry() *Registry { + return &Registry{drivers: make(map[string]Driver)} +} + +// Register adds a driver to the registry. Returns an error if a driver with +// the same name is already registered. +func (r *Registry) Register(d Driver) error { + r.mu.Lock() + defer r.mu.Unlock() + name := d.Name() + if _, exists := r.drivers[name]; exists { + return fmt.Errorf("driver %q already registered", name) + } + r.drivers[name] = d + return nil +} + +// MustRegister is like Register but panics on error. +func (r *Registry) MustRegister(d Driver) { + if err := r.Register(d); err != nil { + panic(err) + } +} + +// Get returns the driver with the given name, or an error if not found. +func (r *Registry) Get(name string) (Driver, error) { + r.mu.RLock() + defer r.mu.RUnlock() + d, ok := r.drivers[name] + if !ok { + return nil, fmt.Errorf("driver %q not found; registered: %v", name, r.names()) + } + return d, nil +} + +// Names returns the sorted list of registered driver names. +func (r *Registry) Names() []string { + r.mu.RLock() + defer r.mu.RUnlock() + return r.names() +} + +func (r *Registry) names() []string { + names := make([]string, 0, len(r.drivers)) + for n := range r.drivers { + names = append(names, n) + } + return names +} diff --git a/pkg/testharness/harness.go b/pkg/testharness/harness.go new file mode 100644 index 0000000..a02c9e2 --- /dev/null +++ b/pkg/testharness/harness.go @@ -0,0 +1,54 @@ +// Package testharness provides a Postgres test harness for migration driver tests. +// It auto-selects an appropriate backend: +// 1. ProvidedDSN — if WORKFLOW_MIGRATE_TEST_DSN env var is set. +// 2. EmbeddedPostgres — Fergus Strange's pure-Go embedded Postgres (default, no Docker required). +package testharness + +import ( + "fmt" + "os" + "testing" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" +) + +// Harness wraps a running Postgres instance. +type Harness struct { + dsn string + embedded *embeddedpostgres.EmbeddedPostgres +} + +// New creates and starts a new Postgres harness. +// The caller must defer h.Close(t) to stop the server. +func New() (*Harness, error) { + // 1. Provided DSN. + if dsn := os.Getenv("WORKFLOW_MIGRATE_TEST_DSN"); dsn != "" { + return &Harness{dsn: dsn}, nil + } + + // 2. Embedded Postgres. + ep := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). + Username("test"). + Password("test"). + Database("migrations_test"). + Port(15432), + ) + if err := ep.Start(); err != nil { + return nil, fmt.Errorf("embedded-postgres start: %w", err) + } + dsn := "postgres://test:test@localhost:15432/migrations_test?sslmode=disable" + return &Harness{dsn: dsn, embedded: ep}, nil +} + +// DSN returns the PostgreSQL connection string for this harness. +func (h *Harness) DSN() string { return h.dsn } + +// Close stops the embedded Postgres server, if any. +func (h *Harness) Close(t *testing.T) { + t.Helper() + if h.embedded != nil { + if err := h.embedded.Stop(); err != nil { + t.Logf("embedded-postgres stop: %v", err) + } + } +} diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..e0e5a53 --- /dev/null +++ b/plugin.json @@ -0,0 +1,43 @@ +{ + "name": "workflow-plugin-migrations", + "version": "0.0.0", + "description": "Database migration plugin for the workflow engine: golang-migrate + goose drivers, pre-deploy runner, wfctl migrate CLI", + "author": "GoCodeAlone", + "license": "Apache-2.0", + "type": "external", + "tier": "community", + "private": false, + "minEngineVersion": "0.18.0", + "keywords": ["migrations", "database", "postgres", "golang-migrate", "goose"], + "homepage": "https://github.com/GoCodeAlone/workflow-plugin-migrations", + "repository": "https://github.com/GoCodeAlone/workflow-plugin-migrations", + "capabilities": { + "configProvider": false, + "moduleTypes": [ + "database.migrations", + "database.migration_driver" + ], + "stepTypes": [ + "step.migrate_up", + "step.migrate_down", + "step.migrate_status", + "step.migrate_to" + ], + "triggerTypes": [], + "migrationDrivers": ["golang-migrate", "goose"], + "cliCommands": [ + { + "name": "migrate", + "description": "Run database migrations", + "subcommands": [ + {"name": "up", "description": "Apply all pending migrations"}, + {"name": "down", "description": "Roll back migrations"}, + {"name": "status", "description": "Show migration status"}, + {"name": "goto", "description": "Migrate to a specific version"} + ], + "flags_passthrough": true + } + ], + "buildHooks": [] + } +} From ebd27f764ba62fba718560921570994b7dcc4d98 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 20:22:03 -0400 Subject: [PATCH 02/10] fix: gofmt internal/module_driver.go (trailing whitespace in aligned one-liners) Co-Authored-By: Claude Sonnet 4.6 --- internal/module_driver.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/module_driver.go b/internal/module_driver.go index 0869725..5f1a6ed 100644 --- a/internal/module_driver.go +++ b/internal/module_driver.go @@ -37,9 +37,9 @@ func newDriverModule(name string, rawCfg map[string]any) (sdk.ModuleInstance, er return &driverModule{name: name, cfg: cfg}, nil } -func (m *driverModule) Init() error { return nil } -func (m *driverModule) Start(_ context.Context) error { return nil } -func (m *driverModule) Stop(_ context.Context) error { return nil } +func (m *driverModule) Init() error { return nil } +func (m *driverModule) Start(_ context.Context) error { return nil } +func (m *driverModule) Stop(_ context.Context) error { return nil } // InvokeMethod returns driver metadata. func (m *driverModule) InvokeMethod(method string, _ map[string]any) (map[string]any, error) { From bda6d254fcbf14222b33e8297a83ea3ec5b321d2 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 20:24:31 -0400 Subject: [PATCH 03/10] fix(goose): pass caller ctx to provider.Status instead of context.Background() Addresses code-review issue: the Status method was discarding its ctx argument and using context.Background(), ignoring any deadline/cancel the caller set. Now provider.Status(ctx) correctly propagates context. Co-Authored-By: Claude Sonnet 4.6 --- internal/goose/driver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/goose/driver.go b/internal/goose/driver.go index 90b742d..647bba0 100644 --- a/internal/goose/driver.go +++ b/internal/goose/driver.go @@ -100,7 +100,7 @@ func (d *Driver) Down(ctx context.Context, req interfaces.MigrationRequest) (int } // Status returns the current migration state. -func (d *Driver) Status(_ context.Context, req interfaces.MigrationRequest) (interfaces.MigrationStatus, error) { +func (d *Driver) Status(ctx context.Context, req interfaces.MigrationRequest) (interfaces.MigrationStatus, error) { if err := req.Validate(); err != nil { return interfaces.MigrationStatus{}, err } @@ -115,7 +115,7 @@ func (d *Driver) Status(_ context.Context, req interfaces.MigrationRequest) (int return interfaces.MigrationStatus{}, err } - migrations, err := provider.Status(context.Background()) + migrations, err := provider.Status(ctx) if err != nil { return interfaces.MigrationStatus{}, fmt.Errorf("goose status: %w", err) } From 180aae6e88c72e5b2136c4b5b0077683d96f134a Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 20:46:56 -0400 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20two=20CI=20failures=20=E2=80=94=20?= =?UTF-8?q?golang-migrate=20URL=20rewrite=20+=20goose=20corpus=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1 (internal/golangmigrate/driver.go): URL scheme rewrite used a hardcoded length check (dsn[:10]) to detect "postgres://", but that prefix is 11 chars — off-by-one meant pgx5:// substitution never fired, causing "unknown driver postgres" in CI. Replaced with strings.HasPrefix. Fix 2 (pkg/conformance): goose v3 expects combined SQL files with -- +goose Up/Down annotations, not paired .up.sql/.down.sql files (those are the golang-migrate format). Added corpus_goose/ with combined files and taught Suite.extractCorpus to select the right corpus by driver name. Also updated internal/goose/driver_test.go to use combined format. Co-Authored-By: Claude Sonnet 4.6 --- internal/golangmigrate/driver.go | 8 +++-- internal/goose/driver_test.go | 13 ++------ .../corpus_goose/00001_create_users.sql | 9 ++++++ .../corpus_goose/00002_add_indexes.sql | 9 ++++++ pkg/conformance/suite.go | 32 ++++++++++++++----- 5 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 pkg/conformance/corpus_goose/00001_create_users.sql create mode 100644 pkg/conformance/corpus_goose/00002_add_indexes.sql diff --git a/internal/golangmigrate/driver.go b/internal/golangmigrate/driver.go index 1955d9c..7b137c7 100644 --- a/internal/golangmigrate/driver.go +++ b/internal/golangmigrate/driver.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "github.com/golang-migrate/migrate/v4" @@ -143,10 +144,11 @@ func (d *Driver) Goto(_ context.Context, req interfaces.MigrationRequest, target // the pgx/v5 driver. func newMigrate(req interfaces.MigrationRequest) (*migrate.Migrate, error) { dsn := req.DSN - // golang-migrate pgx/v5 driver expects pgx5:// scheme. - if len(dsn) >= 10 && dsn[:10] == "postgres://" { + // golang-migrate pgx/v5 driver registers as "pgx5" and expects pgx5:// scheme. + switch { + case strings.HasPrefix(dsn, "postgres://"): dsn = "pgx5://" + dsn[len("postgres://"):] - } else if len(dsn) >= 13 && dsn[:13] == "postgresql://" { + case strings.HasPrefix(dsn, "postgresql://"): dsn = "pgx5://" + dsn[len("postgresql://"):] } diff --git a/internal/goose/driver_test.go b/internal/goose/driver_test.go index 58944f1..c11c3fe 100644 --- a/internal/goose/driver_test.go +++ b/internal/goose/driver_test.go @@ -30,16 +30,9 @@ func TestGooseDriver_UpDownStatus(t *testing.T) { defer h.Close(t) dir := t.TempDir() - writeGooseSQL(t, dir, "00001_users.sql", `-- +goose Up -CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL); --- +goose Down -DROP TABLE IF EXISTS users; -`) - writeGooseSQL(t, dir, "00002_posts.sql", `-- +goose Up -CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT NOT NULL); --- +goose Down -DROP TABLE IF EXISTS posts; -`) + // Goose v3 expects combined SQL files with -- +goose Up/Down annotations. + writeGooseSQL(t, dir, "00001_users.sql", "-- +goose Up\nCREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL);\n-- +goose Down\nDROP TABLE IF EXISTS users;\n") + writeGooseSQL(t, dir, "00002_posts.sql", "-- +goose Up\nCREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT NOT NULL);\n-- +goose Down\nDROP TABLE IF EXISTS posts;\n") ctx := context.Background() d := goosedriver.New() diff --git a/pkg/conformance/corpus_goose/00001_create_users.sql b/pkg/conformance/corpus_goose/00001_create_users.sql new file mode 100644 index 0000000..644c0ff --- /dev/null +++ b/pkg/conformance/corpus_goose/00001_create_users.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL +); + +-- +goose Down +DROP TABLE IF EXISTS users; diff --git a/pkg/conformance/corpus_goose/00002_add_indexes.sql b/pkg/conformance/corpus_goose/00002_add_indexes.sql new file mode 100644 index 0000000..1b48b62 --- /dev/null +++ b/pkg/conformance/corpus_goose/00002_add_indexes.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_users_email ON users (email); +CREATE INDEX IF NOT EXISTS idx_users_name ON users (name); +-- +goose StatementEnd + +-- +goose Down +DROP INDEX IF EXISTS idx_users_email; +DROP INDEX IF EXISTS idx_users_name; diff --git a/pkg/conformance/suite.go b/pkg/conformance/suite.go index fd0eb71..867b518 100644 --- a/pkg/conformance/suite.go +++ b/pkg/conformance/suite.go @@ -17,11 +17,14 @@ import ( //go:embed corpus var corpusFS embed.FS +//go:embed corpus_goose +var corpusGooseFS embed.FS + // Suite runs the conformance test matrix against a given MigrationDriver. type Suite struct { // DSN is the PostgreSQL connection string to use. DSN string - // SourceDir is overridden per sub-test with a temp dir containing the corpus. + // sourceDir is set per-run with the extracted corpus. sourceDir string } @@ -31,9 +34,11 @@ func NewSuite(dsn string) *Suite { } // Run executes all conformance cases against the given driver. +// The corpus format is selected based on the driver name (goose uses combined +// SQL files with -- +goose Up/Down annotations; others use paired .up.sql/.down.sql files). func (s *Suite) Run(t *testing.T, d interfaces.MigrationDriver) { t.Helper() - dir := s.extractCorpus(t) + dir := s.extractCorpus(t, d.Name()) s.sourceDir = dir t.Run("FreshUpAll", func(t *testing.T) { s.testFreshUpAll(t, d) }) @@ -81,7 +86,6 @@ func (s *Suite) testIdempotentUp(t *testing.T, d interfaces.MigrationDriver) { t.Helper() ctx := context.Background() - // Apply all first. if _, err := d.Up(ctx, s.req()); err != nil { t.Fatalf("initial Up() error: %v", err) } @@ -189,16 +193,28 @@ func (s *Suite) testStatusReflectsState(t *testing.T, d interfaces.MigrationDriv } } -// extractCorpus copies embedded corpus SQL files to a temporary directory and -// returns its path. -func (s *Suite) extractCorpus(t *testing.T) string { +// extractCorpus copies the appropriate embedded corpus SQL files to a temporary +// directory and returns its path. driverName selects between corpus formats: +// "goose" gets combined -- +goose Up/Down files; others get paired .up.sql/.down.sql. +func (s *Suite) extractCorpus(t *testing.T, driverName string) string { t.Helper() dir := t.TempDir() - err := fs.WalkDir(corpusFS, "corpus", func(path string, de fs.DirEntry, err error) error { + + var srcFS embed.FS + var srcDir string + if driverName == "goose" { + srcFS = corpusGooseFS + srcDir = "corpus_goose" + } else { + srcFS = corpusFS + srcDir = "corpus" + } + + err := fs.WalkDir(srcFS, srcDir, func(path string, de fs.DirEntry, err error) error { if err != nil || de.IsDir() { return err } - data, err := corpusFS.ReadFile(path) + data, err := srcFS.ReadFile(path) if err != nil { return err } From 6f9c208634fa556d057c154ce02a8deab159a4a3 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 20:59:24 -0400 Subject: [PATCH 05/10] fix(testharness): per-test schema isolation to prevent CI state pollution Each New() call now creates a unique PostgreSQL schema (wfm_test__) and appends search_path= to the DSN so all tables land in an isolated namespace. Close(t) drops the schema CASCADE. This eliminates cross-test relation-already-exists and dirty-database failures when multiple test files run against the same postgres instance. Also remove -- +goose StatementBegin/End wrapper from corpus_goose migration 2 since CREATE INDEX cannot run inside a transaction block. Co-Authored-By: Claude Sonnet 4.6 --- .../corpus_goose/00002_add_indexes.sql | 2 - pkg/testharness/harness.go | 110 ++++++++++++++---- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/pkg/conformance/corpus_goose/00002_add_indexes.sql b/pkg/conformance/corpus_goose/00002_add_indexes.sql index 1b48b62..57384b9 100644 --- a/pkg/conformance/corpus_goose/00002_add_indexes.sql +++ b/pkg/conformance/corpus_goose/00002_add_indexes.sql @@ -1,8 +1,6 @@ -- +goose Up --- +goose StatementBegin CREATE INDEX IF NOT EXISTS idx_users_email ON users (email); CREATE INDEX IF NOT EXISTS idx_users_name ON users (name); --- +goose StatementEnd -- +goose Down DROP INDEX IF EXISTS idx_users_email; diff --git a/pkg/testharness/harness.go b/pkg/testharness/harness.go index a02c9e2..7195ab1 100644 --- a/pkg/testharness/harness.go +++ b/pkg/testharness/harness.go @@ -1,54 +1,122 @@ // Package testharness provides a Postgres test harness for migration driver tests. -// It auto-selects an appropriate backend: +// Each call to New() creates an isolated PostgreSQL schema so tests don't +// pollute each other's state. +// +// Backend selection: // 1. ProvidedDSN — if WORKFLOW_MIGRATE_TEST_DSN env var is set. -// 2. EmbeddedPostgres — Fergus Strange's pure-Go embedded Postgres (default, no Docker required). +// 2. EmbeddedPostgres — Fergus Strange's pure-Go embedded Postgres (default). package testharness import ( + "context" + "database/sql" "fmt" + "math/rand" "os" + "strings" "testing" + "time" embeddedpostgres "github.com/fergusstrange/embedded-postgres" + _ "github.com/jackc/pgx/v5/stdlib" ) -// Harness wraps a running Postgres instance. +// Harness wraps a running Postgres instance with an isolated schema. type Harness struct { - dsn string + // dsn is the connection string with search_path set to the isolated schema. + dsn string + // schema is the unique schema name created for this harness instance. + schema string + // adminConn is used to create and drop the schema. + adminConn *sql.DB + // embedded is non-nil when we started the embedded postgres. embedded *embeddedpostgres.EmbeddedPostgres } -// New creates and starts a new Postgres harness. -// The caller must defer h.Close(t) to stop the server. +// New creates and starts a new Postgres harness with an isolated schema. +// The caller must defer h.Close(t) to clean up the schema and stop the server. func New() (*Harness, error) { - // 1. Provided DSN. - if dsn := os.Getenv("WORKFLOW_MIGRATE_TEST_DSN"); dsn != "" { - return &Harness{dsn: dsn}, nil + baseDSN := os.Getenv("WORKFLOW_MIGRATE_TEST_DSN") + + var ep *embeddedpostgres.EmbeddedPostgres + if baseDSN == "" { + // Start embedded Postgres on a random port to avoid conflicts. + port := uint32(15432 + rand.Intn(1000)) + ep = embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). + Username("test"). + Password("test"). + Database("migrations_test"). + Port(port), + ) + if err := ep.Start(); err != nil { + return nil, fmt.Errorf("embedded-postgres start: %w", err) + } + baseDSN = fmt.Sprintf("postgres://test:test@localhost:%d/migrations_test?sslmode=disable", port) + } + + // Create a unique schema for this harness to isolate from other tests. + schema := uniqueSchema() + adminConn, err := sql.Open("pgx", baseDSN) + if err != nil { + if ep != nil { + _ = ep.Stop() + } + return nil, fmt.Errorf("testharness: open admin conn: %w", err) } - // 2. Embedded Postgres. - ep := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). - Username("test"). - Password("test"). - Database("migrations_test"). - Port(15432), - ) - if err := ep.Start(); err != nil { - return nil, fmt.Errorf("embedded-postgres start: %w", err) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if _, err := adminConn.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA %q", schema)); err != nil { + _ = adminConn.Close() + if ep != nil { + _ = ep.Stop() + } + return nil, fmt.Errorf("testharness: create schema %q: %w", schema, err) } - dsn := "postgres://test:test@localhost:15432/migrations_test?sslmode=disable" - return &Harness{dsn: dsn, embedded: ep}, nil + + // Build a DSN that sets search_path so all tables land in our schema. + schemaDSN := withSearchPath(baseDSN, schema) + + return &Harness{ + dsn: schemaDSN, + schema: schema, + adminConn: adminConn, + embedded: ep, + }, nil } // DSN returns the PostgreSQL connection string for this harness. +// The DSN includes search_path= for isolation. func (h *Harness) DSN() string { return h.dsn } -// Close stops the embedded Postgres server, if any. +// Close drops the isolated schema and stops the embedded Postgres server if any. func (h *Harness) Close(t *testing.T) { t.Helper() + if h.adminConn != nil && h.schema != "" { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if _, err := h.adminConn.ExecContext(ctx, fmt.Sprintf("DROP SCHEMA %q CASCADE", h.schema)); err != nil { + t.Logf("testharness: drop schema %q: %v", h.schema, err) + } + _ = h.adminConn.Close() + } if h.embedded != nil { if err := h.embedded.Stop(); err != nil { t.Logf("embedded-postgres stop: %v", err) } } } + +// uniqueSchema generates a unique schema name for this test run. +func uniqueSchema() string { + return fmt.Sprintf("wfm_test_%d_%d", time.Now().UnixNano(), rand.Int63n(9999)) +} + +// withSearchPath appends search_path= to a postgres DSN. +// It handles both DSNs with and without existing query parameters. +func withSearchPath(dsn, schema string) string { + if strings.Contains(dsn, "?") { + return dsn + "&search_path=" + schema + } + return dsn + "?search_path=" + schema +} From 6d19d9d91f1b236238191887c1fa9291109f6321 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 21:09:07 -0400 Subject: [PATCH 06/10] fix(conformance): remove CONCURRENTLY from golang-migrate corpus index migration CREATE INDEX CONCURRENTLY cannot run inside a transaction block, which is how golang-migrate/v4 wraps each migration by default. The conformance suite tests driver behavior, not production indexing strategy, so plain CREATE INDEX is sufficient. Co-Authored-By: Claude Sonnet 4.6 --- pkg/conformance/corpus/002_add_indexes.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/conformance/corpus/002_add_indexes.up.sql b/pkg/conformance/corpus/002_add_indexes.up.sql index 885bd98..e8a38f6 100644 --- a/pkg/conformance/corpus/002_add_indexes.up.sql +++ b/pkg/conformance/corpus/002_add_indexes.up.sql @@ -1,2 +1,2 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email); -CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_name ON users (name); +CREATE INDEX IF NOT EXISTS idx_users_email ON users (email); +CREATE INDEX IF NOT EXISTS idx_users_name ON users (name); From 23a946976204a684eadc830613589138b3cd22e9 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 21:18:20 -0400 Subject: [PATCH 07/10] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20driver=20correctness=20and=20module=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - registry: sort Names() return value (comment claimed sorted; now it is) - golangmigrate: Down() returns actual rolled-back version strings and empty slice on no-op (ErrNoChange), not a synthetic summary string - module_migrations: apply context.WithTimeout so driver calls respect the configured timeout instead of running on context.Background() indefinitely - module_migrations: simplify dsn_env defaulting — always default to DATABASE_URL; remove dead rawCfg["dsn"] check in constructor - module_migrations: clarify driver_ref docstring (module lookup not yet impl) Co-Authored-By: Claude Sonnet 4.6 --- internal/golangmigrate/driver.go | 20 ++++++++++++++++---- internal/module_migrations.go | 15 +++++++-------- pkg/driver/registry.go | 2 ++ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/internal/golangmigrate/driver.go b/internal/golangmigrate/driver.go index 7b137c7..68fca21 100644 --- a/internal/golangmigrate/driver.go +++ b/internal/golangmigrate/driver.go @@ -75,12 +75,24 @@ func (d *Driver) Down(ctx context.Context, req interfaces.MigrationRequest) (int return interfaces.MigrationResult{}, fmt.Errorf("golang-migrate down: %w", err) } - after, _, _ := m.Version() - _ = before - _ = after + after, _, afterErr := m.Version() + + // Build list of rolled-back version strings (highest to lowest). + // If we're back at nil version (ErrNilVersion), treat after as 0. + var rolledBack []string + if errors.Is(afterErr, migrate.ErrNilVersion) { + for v := uint(1); v <= before; v++ { + rolledBack = append(rolledBack, fmt.Sprintf("%d", v)) + } + } else if after < before { + for v := after + 1; v <= before; v++ { + rolledBack = append(rolledBack, fmt.Sprintf("%d", v)) + } + } + // If after >= before, nothing was rolled back — return empty slice. return interfaces.MigrationResult{ - Applied: []string{fmt.Sprintf("rolled back %d migration(s)", steps)}, + Applied: rolledBack, DurationMs: time.Since(start).Milliseconds(), }, nil } diff --git a/internal/module_migrations.go b/internal/module_migrations.go index f804b06..7be4e54 100644 --- a/internal/module_migrations.go +++ b/internal/module_migrations.go @@ -15,8 +15,8 @@ import ( // migrationsModuleConfig is the configuration for the database.migrations module. type migrationsModuleConfig struct { - // DriverRef is the name of a database.migration_driver module, OR a driver - // name (golang-migrate, goose) for inline driver configuration. + // DriverRef is treated as a literal driver name (golang-migrate, goose). + // Module-reference lookup is not yet implemented. DriverRef string `json:"driver_ref"` // Driver is the driver name when not using a module ref. Driver string `json:"driver"` @@ -62,11 +62,9 @@ func newMigrationsModule(name string, rawCfg map[string]any) (sdk.ModuleInstance if cfg.SourceDir == "" { return nil, fmt.Errorf("database.migrations %q: source_dir is required", name) } - if cfg.DSNEnv == "" && rawCfg["dsn"] == nil { - // Allow direct dsn for test usage - if v, ok := rawCfg["dsn"].(string); !ok || v == "" { - cfg.DSNEnv = "DATABASE_URL" - } + // Default DSN env to DATABASE_URL; runtime args["dsn"] can override at call time. + if cfg.DSNEnv == "" { + cfg.DSNEnv = "DATABASE_URL" } return &migrationsModule{name: name, cfg: cfg}, nil } @@ -122,7 +120,8 @@ func (m *migrationsModule) InvokeMethod(method string, args map[string]any) (map }, } - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() switch method { case "up": diff --git a/pkg/driver/registry.go b/pkg/driver/registry.go index e7ae26f..4d89e15 100644 --- a/pkg/driver/registry.go +++ b/pkg/driver/registry.go @@ -2,6 +2,7 @@ package driver import ( "fmt" + "sort" "sync" ) @@ -59,5 +60,6 @@ func (r *Registry) names() []string { for n := range r.drivers { names = append(names, n) } + sort.Strings(names) return names } From 1feb285524401770bfed3afeebee92ff26eecb18 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 21:18:26 -0400 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20build,=20port=20selection,=20test=20error=20handlin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/workflow-plugin-migrations: add var version string for goreleaser ldflag - cmd/workflow-plugin-atlas-migrate: add var version string for goreleaser ldflag - testharness: replace rand.Intn port selection with net.Listen(":0") to atomically acquire a free port and avoid collisions in parallel test processes - conformance: handle Status() error in testDownOne instead of silently discarding it with blank identifier Co-Authored-By: Claude Sonnet 4.6 --- cmd/workflow-plugin-atlas-migrate/main.go | 3 +++ cmd/workflow-plugin-migrations/main.go | 3 +++ pkg/conformance/suite.go | 5 ++++- pkg/testharness/harness.go | 26 ++++++++++++++++++++--- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/cmd/workflow-plugin-atlas-migrate/main.go b/cmd/workflow-plugin-atlas-migrate/main.go index 2f27377..05692c6 100644 --- a/cmd/workflow-plugin-atlas-migrate/main.go +++ b/cmd/workflow-plugin-atlas-migrate/main.go @@ -9,6 +9,9 @@ import ( sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) +// version is set at build time via -X main.version=. +var version string + func main() { sdk.Serve(atlasplugin.NewPlugin()) } diff --git a/cmd/workflow-plugin-migrations/main.go b/cmd/workflow-plugin-migrations/main.go index 90be3f0..29f00db 100644 --- a/cmd/workflow-plugin-migrations/main.go +++ b/cmd/workflow-plugin-migrations/main.go @@ -13,6 +13,9 @@ import ( sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) +// version is set at build time via -X main.version=. +var version string + func main() { sdk.ServePluginFull(internal.NewPlugin(), cli.NewCLIProvider(), nil) } diff --git a/pkg/conformance/suite.go b/pkg/conformance/suite.go index 867b518..42b5033 100644 --- a/pkg/conformance/suite.go +++ b/pkg/conformance/suite.go @@ -109,7 +109,10 @@ func (s *Suite) testDownOne(t *testing.T, d interfaces.MigrationDriver) { t.Fatalf("Up() error: %v", err) } - stBefore, _ := d.Status(ctx, s.req()) + stBefore, err := d.Status(ctx, s.req()) + if err != nil { + t.Fatalf("Status() before Down error: %v", err) + } req := s.req() req.Options.Steps = 1 diff --git a/pkg/testharness/harness.go b/pkg/testharness/harness.go index 7195ab1..96fc851 100644 --- a/pkg/testharness/harness.go +++ b/pkg/testharness/harness.go @@ -12,6 +12,7 @@ import ( "database/sql" "fmt" "math/rand" + "net" "os" "strings" "testing" @@ -40,13 +41,18 @@ func New() (*Harness, error) { var ep *embeddedpostgres.EmbeddedPostgres if baseDSN == "" { - // Start embedded Postgres on a random port to avoid conflicts. - port := uint32(15432 + rand.Intn(1000)) + // Pick a free port atomically via net.Listen so parallel test processes + // never collide (math/rand port selection can produce the same value + // across processes even when auto-seeded). + port, err := freePort() + if err != nil { + return nil, fmt.Errorf("testharness: find free port: %w", err) + } ep = embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). Username("test"). Password("test"). Database("migrations_test"). - Port(port), + Port(uint32(port)), ) if err := ep.Start(); err != nil { return nil, fmt.Errorf("embedded-postgres start: %w", err) @@ -54,6 +60,7 @@ func New() (*Harness, error) { baseDSN = fmt.Sprintf("postgres://test:test@localhost:%d/migrations_test?sslmode=disable", port) } + // Create a unique schema for this harness to isolate from other tests. schema := uniqueSchema() adminConn, err := sql.Open("pgx", baseDSN) @@ -107,6 +114,19 @@ func (h *Harness) Close(t *testing.T) { } } +// freePort asks the OS for an unused TCP port and returns it. +// Using net.Listen avoids port collisions that can occur when multiple test +// processes generate random port numbers concurrently. +func freePort() (int, error) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + port := l.Addr().(*net.TCPAddr).Port + _ = l.Close() + return port, nil +} + // uniqueSchema generates a unique schema name for this test run. func uniqueSchema() string { return fmt.Sprintf("wfm_test_%d_%d", time.Now().UnixNano(), rand.Int63n(9999)) From d5a1d097745299a93edbd61ef74e35014834c74c Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 21:20:54 -0400 Subject: [PATCH 09/10] fix(golangmigrate): populate Pending in Status() via file source enumeration golang-migrate has no built-in API to list pending migrations. Open the file source directly (migratefile.File.Open) and walk First/Next to collect versions that exceed the current applied version. This fixes TestConformance/StatusReflectsState which was failing because Pending was always empty. Co-Authored-By: Claude Sonnet 4.6 --- internal/golangmigrate/driver.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/internal/golangmigrate/driver.go b/internal/golangmigrate/driver.go index 68fca21..fa9e8e7 100644 --- a/internal/golangmigrate/driver.go +++ b/internal/golangmigrate/driver.go @@ -10,7 +10,7 @@ import ( "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/pgx/v5" - _ "github.com/golang-migrate/migrate/v4/source/file" + migratefile "github.com/golang-migrate/migrate/v4/source/file" "github.com/GoCodeAlone/workflow/interfaces" ) @@ -114,12 +114,17 @@ func (d *Driver) Status(_ context.Context, req interfaces.MigrationRequest) (int } current := "" - if !errors.Is(err, migrate.ErrNilVersion) { + atNil := errors.Is(err, migrate.ErrNilVersion) + if !atNil { current = fmt.Sprintf("%d", version) } + // Enumerate pending migrations (versions in source that exceed current). + pending, _ := listPendingVersions(req.Source.Dir, version, atNil) + return interfaces.MigrationStatus{ Current: current, + Pending: pending, Dirty: dirty, }, nil } @@ -180,3 +185,25 @@ func collectApplied(before, after uint) []string { } return applied } + +// listPendingVersions opens the file source and returns the version strings of +// migrations that have not yet been applied (i.e. version > current). +// When atNil is true the DB has no applied migrations, so every version is pending. +func listPendingVersions(dir string, current uint, atNil bool) ([]string, error) { + src := &migratefile.File{} + s, err := src.Open("file://" + dir) + if err != nil { + return nil, fmt.Errorf("golang-migrate: open source for pending list: %w", err) + } + defer s.Close() + + var pending []string + v, err := s.First() + for err == nil { + if atNil || v > current { + pending = append(pending, fmt.Sprintf("%d", v)) + } + v, err = s.Next(v) + } + return pending, nil +} From cd190ffd51de3861a2973f2de97e7523e0d5e6a1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 21:21:12 -0400 Subject: [PATCH 10/10] style: gofmt harness.go (remove double blank line) Co-Authored-By: Claude Sonnet 4.6 --- pkg/testharness/harness.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/testharness/harness.go b/pkg/testharness/harness.go index 96fc851..c026cf9 100644 --- a/pkg/testharness/harness.go +++ b/pkg/testharness/harness.go @@ -60,7 +60,6 @@ func New() (*Harness, error) { baseDSN = fmt.Sprintf("postgres://test:test@localhost:%d/migrations_test?sslmode=disable", port) } - // Create a unique schema for this harness to isolate from other tests. schema := uniqueSchema() adminConn, err := sql.Open("pgx", baseDSN)