From 9af0987ceedc756bf526107f1b6448747f1c91c8 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Wed, 28 Oct 2026 16:12:31 -0400 Subject: [PATCH 1/6] feat: the solution-go service agent (WIP, rescued from scratchpad) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scaffolds, builds and runs a solution backend, so `codefly add solution` writes code and not only the five YAML documents. Builds and vets clean; one test fails — TestThePageSharesEveryHostSingleton, "the shared policy does not seal react to the host's instance" — which is unfinished work, not a regression. Pushed to keep it off a scratchpad that can vanish. Refs codefly-dev/service-solution-go#1 Co-Authored-By: Claude Opus 5 --- .goreleaser.yaml | 72 +++ agent.codefly.yaml | 15 + bootstrap-agent | 8 + builder.go | 127 +++++ consumption.go | 143 ++++++ go.mod | 162 +++++++ go.sum | 436 ++++++++++++++++++ main.go | 96 ++++ scaffold.go | 252 ++++++++++ scaffold_test.go | 280 +++++++++++ service.go | 110 +++++ shape_test.go | 167 +++++++ templates/agent/README.md.tmpl | 26 ++ templates/builder/Dockerfile.tmpl | 114 +++++ templates/builder/dockerignore.tmpl | 2 + .../kustomize/base/deployment.yaml.tmpl | 79 ++++ .../kustomize/base/kustomization.yaml.tmpl | 6 + .../kustomize/base/namespace.yaml.tmpl | 8 + .../kustomize/base/service.yaml.tmpl | 13 + .../overlays/environment/configmap.yaml.tmpl | 9 + .../environment/kustomization.yaml.tmpl | 6 + .../overlays/environment/secret.yaml.tmpl | 11 + templates/factory/.gitignore.tmpl | 14 + templates/factory/GETTING_STARTED.md.tmpl | 69 +++ templates/factory/consumes.go.tmpl | 98 ++++ templates/factory/consumes_test.go.tmpl | 129 ++++++ templates/factory/go.mod.tmpl | 80 ++++ templates/factory/go.sum.tmpl | 271 +++++++++++ templates/factory/main.go.tmpl | 63 +++ templates/factory/openapi.go.tmpl | 28 ++ templates/factory/openapi_test.go.tmpl | 41 ++ templates/factory/operations.go.tmpl | 40 ++ templates/page/.npmrc.tmpl | 5 + templates/page/README.md.tmpl | 23 + templates/page/package.json.tmpl | 38 ++ templates/page/rsbuild.config.ts.tmpl | 68 +++ templates/page/src/Page.test.tsx.tmpl | 32 ++ templates/page/src/Page.tsx.tmpl | 58 +++ templates/page/src/binding.ts.tmpl | 24 + templates/page/src/page.css.tmpl | 30 ++ templates/page/tsconfig.json.tmpl | 15 + templates/page/vitest.config.ts.tmpl | 13 + 42 files changed, 3281 insertions(+) create mode 100644 .goreleaser.yaml create mode 100644 agent.codefly.yaml create mode 100755 bootstrap-agent create mode 100644 builder.go create mode 100644 consumption.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 scaffold.go create mode 100644 scaffold_test.go create mode 100644 service.go create mode 100644 shape_test.go create mode 100644 templates/agent/README.md.tmpl create mode 100644 templates/builder/Dockerfile.tmpl create mode 100644 templates/builder/dockerignore.tmpl create mode 100644 templates/deployment/kustomize/base/deployment.yaml.tmpl create mode 100644 templates/deployment/kustomize/base/kustomization.yaml.tmpl create mode 100644 templates/deployment/kustomize/base/namespace.yaml.tmpl create mode 100644 templates/deployment/kustomize/base/service.yaml.tmpl create mode 100644 templates/deployment/kustomize/overlays/environment/configmap.yaml.tmpl create mode 100644 templates/deployment/kustomize/overlays/environment/kustomization.yaml.tmpl create mode 100644 templates/deployment/kustomize/overlays/environment/secret.yaml.tmpl create mode 100644 templates/factory/.gitignore.tmpl create mode 100644 templates/factory/GETTING_STARTED.md.tmpl create mode 100644 templates/factory/consumes.go.tmpl create mode 100644 templates/factory/consumes_test.go.tmpl create mode 100644 templates/factory/go.mod.tmpl create mode 100644 templates/factory/go.sum.tmpl create mode 100644 templates/factory/main.go.tmpl create mode 100644 templates/factory/openapi.go.tmpl create mode 100644 templates/factory/openapi_test.go.tmpl create mode 100644 templates/factory/operations.go.tmpl create mode 100644 templates/page/.npmrc.tmpl create mode 100644 templates/page/README.md.tmpl create mode 100644 templates/page/package.json.tmpl create mode 100644 templates/page/rsbuild.config.ts.tmpl create mode 100644 templates/page/src/Page.test.tsx.tmpl create mode 100644 templates/page/src/Page.tsx.tmpl create mode 100644 templates/page/src/binding.ts.tmpl create mode 100644 templates/page/src/page.css.tmpl create mode 100644 templates/page/tsconfig.json.tmpl create mode 100644 templates/page/vitest.config.ts.tmpl diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..b0ab06f --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,72 @@ +version: 2 + +sboms: + - id: archives + artifacts: archive + documents: + - "${artifact}.sbom.json" + disable: false + +project_name: service-solution-go + +# A prerelease tag (a `codefly publish dev` build, v-dev.) is +# published as a GitHub prerelease, never as Latest, so `version: latest` can +# never resolve to a dev build. Release tags are unaffected. +release: + prerelease: auto + +builds: + # ARCHITECTURE: core's authoritative source inspection uses real tree-sitter + # grammars. Agent releases therefore require CGO and the platform compilers + # supplied by goreleaser/goreleaser-cross; disabling CGO would silently make + # the production parser impossible to ship. + - id: darwin-amd64 + binary: service-solution-go + env: + - CGO_ENABLED=1 + - CC=o64-clang + - CXX=o64-clang++ + goos: + - darwin + goarch: + - amd64 + - id: darwin-arm64 + binary: service-solution-go + env: + - CGO_ENABLED=1 + - CC=oa64-clang + - CXX=oa64-clang++ + goos: + - darwin + goarch: + - arm64 + - id: linux-amd64 + binary: service-solution-go + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + - CXX=x86_64-linux-gnu-g++ + goos: + - linux + goarch: + - amd64 + - id: linux-arm64 + binary: service-solution-go + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + goos: + - linux + goarch: + - arm64 + +archives: + - id: default + formats: + - tar.gz + # Must match core's downloader: service-{name}_{version}_{os}_{arch}.tar.gz + # (resources.AgentKindRegistration.GitHubAsset); the binary inside is + # service-{name}. For this agent {name} is solution-go, so both are + # service-solution-go. release_test.go pins it. + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" diff --git a/agent.codefly.yaml b/agent.codefly.yaml new file mode 100644 index 0000000..a17b094 --- /dev/null +++ b/agent.codefly.yaml @@ -0,0 +1,15 @@ +publisher: codefly.dev +kind: codefly:service +name: solution-go +version: 0.0.1 +release: + owner: workflow + workflow: releaser.yml +# Self-hosted: this agent is a Go program, so it builds itself for a host with +# no codefly.dev service agent installed yet (docs/agent-compatibility.md, +# "Self-hosted packaging"). Naming another agent's release here would make +# every release of THIS agent wait on a bump of that one. +source: + directory: . + agent: self + bootstrap: [./bootstrap-agent] diff --git a/bootstrap-agent b/bootstrap-agent new file mode 100755 index 0000000..358bc47 --- /dev/null +++ b/bootstrap-agent @@ -0,0 +1,8 @@ +#!/bin/sh +# Builds this agent for a host that has no codefly.dev service agent release +# installed yet. Core's source inspection links real tree-sitter grammars, so +# the executable must keep CGO enabled to parse anything. +set -eu + +CGO_ENABLED=1 GOOS="$CODEFLY_AGENT_OS" GOARCH="$CODEFLY_AGENT_ARCH" \ + go build -o "$CODEFLY_AGENT_OUTPUT" . diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..b721b88 --- /dev/null +++ b/builder.go @@ -0,0 +1,127 @@ +package main + +import ( + "context" + "embed" + + builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0" + gobuilder "github.com/codefly-dev/service-go/pkg/builder" +) + +// GoVersion is the exact Go patch release a solution backend is built with, +// in the builder stage and in the scaffolded go.mod alike. +const GoVersion = "1.27.3" + +// AlpineVersion is the exact runtime Alpine patch release the image is built +// on. +const AlpineVersion = "3.23.5" + +// Builder is the solution backend's builder. Load, Init, Build and Deploy are +// the generic Go agent's — a solution backend is compiled, imaged and +// deployed exactly as any Go service is, and a second copy of any of those +// here would be a second mechanism for a job that seam already does. +// +// Create and Sync are this agent's, and they are the whole of what +// `solution-go` is: Create writes a solution backend instead of a plain Go +// service, and Sync keeps the two files derived from the solution's own +// declarations current. +type Builder struct { + *gobuilder.Builder + + service *Service + factory embed.FS + page embed.FS +} + +// NewBuilder wires the generic Go builder with this agent's template trees. +func NewBuilder(svc *Service) *Builder { + return &Builder{ + Builder: gobuilder.New(svc.Service, gobuilder.BuildConfig{ + FactoryFS: factoryFS, + BuilderFS: builderFS, + DeploymentFS: deploymentFS, + Requirements: requirements, + GoVersion: GoVersion, + AlpineVersion: AlpineVersion, + }), + service: svc, + factory: factoryFS, + page: pageFS, + } +} + +// Create scaffolds a solution backend and the ./Page remote beside it. +// +// The order matters and is the point of the whole file: +// +// 1. the templates, which are the author's from the moment they land; +// 2. consumed_gen.go, rendered from api.consumes — which does not exist yet +// under `codefly add solution`, so it is rendered for an empty +// consumption and `codefly sync` fills it in (consumption.go says why); +// 3. the interface document, rendered by the solution runtime's own renderer +// so the committed bytes are the bytes the scaffolded generator writes; +// 4. CreateResponse, which saves the service manifest. +// +// Nothing here declares an endpoint, and that is deliberate. `codefly add +// solution` writes the one endpoint a solution backend serves — public, +// `exposure: none`, with the /health readiness probe this runtime answers — +// together with module-identity and the authority dependencies, and it writes +// them after this returns. An endpoint declared here would be replaced by an +// identical one a moment later, which reads as a disagreement between the +// agent and the CLI where there is none. A solution backend added with +// `codefly add service --agent solution-go`, outside `add solution`, is told +// what to declare by the GETTING_STARTED this Create writes. +func (s *Builder) Create(ctx context.Context, _ *builderv0.CreateRequest) (*builderv0.CreateResponse, error) { + defer s.Wool.Catch() + ctx = s.Wool.Inject(ctx) + + context, err := newScaffold(s.Information) + if err != nil { + return s.Base.Builder.CreateError(err) + } + if err := s.scaffoldSolution(ctx, context); err != nil { + return s.Base.Builder.CreateError(err) + } + if err := s.writeGeneratedConsumption(ctx); err != nil { + return s.Base.Builder.CreateError(err) + } + if err := s.writeInterfaceArtifact(ctx, context, s.serviceVersion()); err != nil { + return s.Base.Builder.CreateError(err) + } + return s.Base.Builder.CreateResponse(ctx, s.service.Settings) +} + +// Sync regenerates what a solution's own declarations project into the +// backend: consumed_gen.go from api.consumes, and the interface document from +// the routes the backend serves. +// +// The generic Go agent's Sync is a no-op — plain Go has nothing to +// regenerate. A solution backend has exactly two derived files, and both go +// stale on a change to a document the CLI owns rather than to any Go source, +// so there is no build that would notice. +// +// The interface document is NOT re-rendered here. Once a backend serves +// routes of its own, only the backend itself can render it — the document is +// rendered from the running declaration — so the scaffolded openapi.go +// carries a `go:generate` line and a test that fails on a stale artifact, and +// `go generate ./...` in the service directory is what moves it. +func (s *Builder) Sync(ctx context.Context, _ *builderv0.SyncRequest) (*builderv0.SyncResponse, error) { + defer s.Wool.Catch() + ctx = s.Wool.Inject(ctx) + + if err := s.writeGeneratedConsumption(ctx); err != nil { + return s.Base.Builder.SyncError(err) + } + return s.Base.Builder.SyncResponse() +} + +// serviceVersion is the version the service manifest declares, which the +// interface document carries. `codefly add service` creates a service at +// 0.0.0 and the solution runtime refuses a document with no version, so the +// zero value is spelled rather than left empty. +func (s *Builder) serviceVersion() string { + if s.Information != nil && s.Information.Agent != nil && s.Base.Service != nil && s.Base.Service.Version != "" { + return s.Base.Service.Version + } + return "0.0.0" +} diff --git a/consumption.go b/consumption.go new file mode 100644 index 0000000..bd5e803 --- /dev/null +++ b/consumption.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/codefly-dev/core/solution/manifest" +) + +// CONSUMPTION IS DECLARED ONCE. +// +// `api.consumes` in solution.codefly.yaml is the one place a solution says +// which modules it may reach: the CLI derives the delivered bindings and the +// solution SDK from it, and core-solutions' check-consumed-apis gate reads it. +// A second list in Go would be a second declaration, and the two have drifted +// in every solution that kept both by hand. +// +// So the Go half is GENERATED from the manifest, as `as` names only — +// consumed_gen.go — and the scaffolded consumes.go derives its declaration +// from that list rather than naming a module itself. The two cannot disagree +// about WHICH modules are consumed, by construction; what consumes.go adds is +// each module's scopes and methods, which no manifest can carry (a method name +// is a constant of the module's generated client). +// +// The generated file is rewritten by `codefly sync`, and the scaffolded +// consumes_test.go fails when the committed file no longer says what the +// manifest does. + +// generatedConsumptionFile is the file this agent owns inside a solution +// backend. Nothing else in the service directory is agent-owned: everything +// else the Create flow writes is the author's from the moment it lands. +const generatedConsumptionFile = "consumed_gen.go" + +// solutionManifestFrom locates the solution manifest for a backend at +// serviceDir. The manifest is the MODULE's, one level above services/ — where +// `codefly add solution` writes it and where core-solutions' gates read it — +// so a backend at /services/backend finds it at /. +func solutionManifestFrom(serviceDir string) string { + return filepath.Join(serviceDir, "..", "..", manifest.FileName) +} + +// consumedAliases reads the `as` of every api.consumes entry, in manifest +// order, from the solution manifest beside the module at serviceDir. +// +// A MISSING MANIFEST IS NOT AN ERROR, and that is deliberate. `codefly add +// solution` scaffolds the services before it writes the documents (cmd/add/ +// solution.go), so at Create time there is no manifest yet and the honest +// answer is "this solution consumes nothing so far". The author's `codefly +// sync` — or the first one CI runs — picks the manifest up. A manifest that +// exists and cannot be read IS an error: silently reading an unreadable +// manifest as "consumes nothing" is how a declaration disappears. +func consumedAliases(serviceDir string) ([]string, error) { + path := solutionManifestFrom(serviceDir) + content, err := os.ReadFile(path) //nolint:gosec // the service's own module directory + if os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("cannot read %s: %w", path, err) + } + // core's own consumption reader, the one every render, sync and authority + // derivation uses. A manifest this refuses is one the platform refuses. + consumption, err := manifest.LoadConsumption(content) + if err != nil { + return nil, fmt.Errorf("%s declares a consumption core cannot read: %w", path, err) + } + aliases := make([]string, 0, len(consumption.API.Consumes)) + for _, consumed := range consumption.API.Consumes { + alias := strings.TrimSpace(consumed.As) + if alias == "" { + alias = strings.TrimSpace(consumed.ID) + } + if alias == "" { + return nil, fmt.Errorf("%s: an api.consumes entry names neither `as` nor `id`, so the solution has no name to address it by", path) + } + aliases = append(aliases, alias) + } + return aliases, nil +} + +// renderConsumption is consumed_gen.go for aliases. It is emitted already +// gofmt-formatted rather than piped through a formatter: the scaffolded drift +// check compares this output against the committed file, and a check whose +// answer depends on whether a formatter is on PATH is not a check. +func renderConsumption(aliases []string) []byte { + var out bytes.Buffer + out.WriteString(`// Code generated from ` + manifest.FileName + ` by the codefly.dev/solution-go +// agent. DO NOT EDIT. +// +// api.consumes is the one place this solution says which modules it may reach. +// These names are that list, so consumes.go cannot name a module the manifest +// does not — nor miss one it does. Regenerate it with ` + "`codefly sync`" + `; +// consumes_test.go fails while this file and the manifest disagree. + +package main + +`) + if len(aliases) == 0 { + out.WriteString("// This solution consumes no module yet. Declare one under api.consumes in " + manifest.FileName + ",\n") + out.WriteString("// then run `codefly sync`.\nvar consumedAsAll = []string{}\n") + return out.Bytes() + } + + width := 0 + for _, alias := range aliases { + if field := aliasField(alias); len(field) > width { + width = len(field) + } + } + out.WriteString("// consumedAs names each consumed module, so consumes.go spells no `as` by hand.\nvar consumedAs = struct {\n") + for _, alias := range aliases { + fmt.Fprintf(&out, "\t%-*s string\n", width, aliasField(alias)) + } + out.WriteString("}{\n") + for _, alias := range aliases { + fmt.Fprintf(&out, "\t%-*s %q,\n", width+1, aliasField(alias)+":", alias) + } + out.WriteString("}\n\n// consumedAsAll is the same list, in manifest order.\nvar consumedAsAll = []string{") + for index, alias := range aliases { + if index > 0 { + out.WriteString(", ") + } + fmt.Fprintf(&out, "%q", alias) + } + out.WriteString("}\n") + return out.Bytes() +} + +// aliasField is the exported Go field name for one `as`. An `as` is a route +// segment (core's manifest validates it as a lowercase id with `.`, `_` or `-` +// separators), so each separated word is capitalised: "document-store" becomes +// DocumentStore. +func aliasField(alias string) string { + var field strings.Builder + for _, word := range strings.FieldsFunc(alias, func(r rune) bool { return r == '-' || r == '_' || r == '.' }) { + field.WriteString(strings.ToUpper(word[:1])) + field.WriteString(word[1:]) + } + return field.String() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d940687 --- /dev/null +++ b/go.mod @@ -0,0 +1,162 @@ +module github.com/codefly-dev/service-solution-go + +go 1.27.3 + +require ( + github.com/codefly-dev/core v0.19.10 + github.com/codefly-dev/service-go v0.0.74 + github.com/codefly-dev/solution-runtime-go v0.0.16 + github.com/stretchr/testify v1.12.1 + google.golang.org/grpc v1.84.0 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + connectrpc.com/connect v1.21.1 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect + github.com/ProtonMail/go-crypto v1.5.2 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cheggaaa/pb/v3 v3.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.5 // indirect + github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec // indirect + github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec // indirect + github.com/codefly-dev/gortk v0.2.0 // indirect + github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 // indirect + github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.2.0 // indirect + github.com/cyphar/filepath-securejoin v0.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dekobon/tree-sitter-groovy v0.3.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.8.2 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.2 // indirect + github.com/go-git/go-git/v5 v5.19.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/analysis v1.0.0 // indirect + github.com/go-openapi/errors v0.22.9 // indirect + github.com/go-openapi/jsonpointer v1.0.1 // indirect + github.com/go-openapi/jsonreference v1.0.2 // indirect + github.com/go-openapi/loads v0.25.3 // indirect + github.com/go-openapi/spec v1.0.1 // indirect + github.com/go-openapi/strfmt v0.27.3 // indirect + github.com/go-openapi/swag v0.29.2 // indirect + github.com/go-openapi/swag/cmdutils v0.29.2 // indirect + github.com/go-openapi/swag/conv v0.29.2 // indirect + github.com/go-openapi/swag/fileutils v0.29.2 // indirect + github.com/go-openapi/swag/jsonutils v0.29.2 // indirect + github.com/go-openapi/swag/loading v0.29.2 // indirect + github.com/go-openapi/swag/mangling v0.29.2 // indirect + github.com/go-openapi/swag/netutils v0.29.2 // indirect + github.com/go-openapi/swag/pools v0.29.2 // indirect + github.com/go-openapi/swag/stringutils v0.29.2 // indirect + github.com/go-openapi/swag/typeutils v0.29.2 // indirect + github.com/go-openapi/swag/yamlutils v0.29.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gofrs/flock v0.13.1 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/go-github/v89 v89.0.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-pointer v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.28 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect + github.com/modelcontextprotocol/go-sdk v1.8.0 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.1.0 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pjbgf/sha1cd v0.7.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect + github.com/scagogogo/python-requirements-parser v0.0.0-20250717025652-6ca77234c827 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.2.2 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect + github.com/tree-sitter-grammars/tree-sitter-kotlin v1.1.0 // indirect + github.com/tree-sitter/go-tree-sitter v0.25.0 // indirect + github.com/tree-sitter/tree-sitter-c-sharp v0.23.5 // indirect + github.com/tree-sitter/tree-sitter-go v0.25.0 // indirect + github.com/tree-sitter/tree-sitter-java v0.23.5 // indirect + github.com/tree-sitter/tree-sitter-javascript v0.25.0 // indirect + github.com/tree-sitter/tree-sitter-python v0.25.0 // indirect + github.com/tree-sitter/tree-sitter-rust v0.24.2 // indirect + github.com/tree-sitter/tree-sitter-typescript v0.23.2 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/yuin/goldmark v1.8.6 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.58.0 // indirect + golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/time v0.16.0 // indirect + golang.org/x/tools v0.52.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect + google.golang.org/protobuf v1.36.12 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6c4b86 --- /dev/null +++ b/go.sum @@ -0,0 +1,436 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +connectrpc.com/connect v1.21.1 h1:h2kacdlzXpZU2OQFSv98xkjYXNUSr/ptxmGVbS/+CRM= +connectrpc.com/connect v1.21.1/go.mod h1:fhMcVJ5w6r/yTYhnkEbW6+sIF7OZ5Ay7KYgmfebh2ek= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= +github.com/ProtonMail/go-crypto v1.5.2 h1:cucYnvqcY7UOXVD//mSyjeaPY0SSN3v5cDkYPxumINk= +github.com/ProtonMail/go-crypto v1.5.2/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +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/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= +github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= +github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec h1:gjLHn5hsteMDx8TeNj+KPBsa06nLwzAEhyR2IfC3JWs= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= +github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec h1:QhncdaWaMERAGpZthlSXjKroXNl/L8llPtBMX+e5w/A= +github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:NqCbvIWbkbCzSpUUYQjP8Jv+LpFnDRbVhPdAj67DHhE= +github.com/codefly-dev/core v0.19.10 h1:rTeh1vph0xGtpiCCt2Dl1AH2Rz9hzIYt1YGcbv6bLYE= +github.com/codefly-dev/core v0.19.10/go.mod h1:aTJY3Rw0OSVkbWRThgDzSkqieoF9mW4MiPRXM8Vzxww= +github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= +github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= +github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 h1:hi+KhotTLMOJ/PUSzVO/Axfho7yZ2UOjrCFfrLI1EKU= +github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458/go.mod h1:Qg8SMzZxKSllwvZwShMrWgSetBVMjxP0s17mwWHWjk0= +github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 h1:wBOL7QsuoC0HsS43gsH2+tpsipk4os077C2VUDkMUHg= +github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458/go.mod h1:hwtrdGtgh8i2xzjgihyd4wY2u+UK7nHQz30OGKL0n+M= +github.com/codefly-dev/service-go v0.0.74 h1:PKFGvGfzyfZsnghwRBz5mfaVR0/glc7kNv5dIDOlC/E= +github.com/codefly-dev/service-go v0.0.74/go.mod h1:QM0Py+nFt81DtyIlQQ9b687vp36m3kAhEauDnYCTli4= +github.com/codefly-dev/solution-runtime-go v0.0.16 h1:I6BpEvzw3PsMYopyDfBCVx2Zcf7yFh6uNqCQcz6MxR0= +github.com/codefly-dev/solution-runtime-go v0.0.16/go.mod h1:sfriXiK34I7k5Ad1Y1VxxNdChsSmzmEJEj3s+IwQlfk= +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.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= +github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= +github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= +github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= +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/dekobon/tree-sitter-groovy v0.3.0 h1:bLi4b7WAQr6Q7nGw529JTHr+jmqvo4yK6nkmGbYQRFU= +github.com/dekobon/tree-sitter-groovy v0.3.0/go.mod h1:5vaBT8Zqmry78Kt/5kluJcvLwRnVR8xtsbGe+K5SAJA= +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/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= +github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +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.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE= +github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4= +github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v1.0.0 h1:sNvbAGCJqUTqIAodr9IVqJMmuZas3YS9ms1dGK9yiJ4= +github.com/go-openapi/analysis v1.0.0/go.mod h1:NhYjJ57fnE+bcE7UwrJyMkhWA3Dfz7TdiBfTVAnos4Y= +github.com/go-openapi/errors v0.22.9 h1:HI9+SyVYiRzyeBQGv0CbWbQa+u2S0nny7G7AhY+UMiM= +github.com/go-openapi/errors v0.22.9/go.mod h1:R73q66sXP2smzAGgFOI4nqtusRK2xN5pYCjsij+KX58= +github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg= +github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY= +github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc= +github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs= +github.com/go-openapi/loads v0.25.3 h1:V+jKy/thXWdLJUuYC8sZX2dICyAa8M3DokF70jj34P0= +github.com/go-openapi/loads v0.25.3/go.mod h1:LgLyCSOLBL2Qnj0Ps1oo2YH8jZoEKsMeVZCc+ALIxFo= +github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk= +github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8= +github.com/go-openapi/strfmt v0.27.3 h1:cyf4J5Wjpd6bFpAs44MF8PsbWF/mWXhn82Ck/hgQZgw= +github.com/go-openapi/strfmt v0.27.3/go.mod h1:Bl+xadPLa3nhBiaCAxNND1vrQfzyEjF0vBrLx96n/4E= +github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= +github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= +github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= +github.com/go-openapi/swag/cmdutils v0.29.2/go.mod h1:XziaSVzYqkDvpWXzASfWajw2YvJrLscq610O2XiyqJw= +github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0= +github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q= +github.com/go-openapi/swag/fileutils v0.29.2 h1:mdUL+Vw5ah1fO1AvFCoHeIyv7YZsCz2MN3KNqd/lLVI= +github.com/go-openapi/swag/fileutils v0.29.2/go.mod h1:7QKmmodjAebaq61lGVJa8ldLdGeMF4DSgYv8tXOGGo0= +github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k= +github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM= +github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY= +github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI= +github.com/go-openapi/swag/mangling v0.29.2 h1:ltdo3K0tTP4P6zWhVVWDU9D37/YYBuMJRdZxiS2Dtog= +github.com/go-openapi/swag/mangling v0.29.2/go.mod h1:RjDi4TnItAenS3cgCXSpIl1kKbjG7il6pnmQVl/8bp8= +github.com/go-openapi/swag/netutils v0.29.2 h1:dBli6jyUa93sDS/XYWShJzf4+nnVfFKfvY9SLUaUzB8= +github.com/go-openapi/swag/netutils v0.29.2/go.mod h1:1pM6Xg/Um8E1eps3umWl8Hme3ByWXOtHsvFC0CULHUE= +github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c= +github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc= +github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk= +github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI= +github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I= +github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE= +github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM= +github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= +github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho= +github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo= +github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE= +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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +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-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0= +github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho= +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/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +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/grpc-ecosystem/grpc-gateway/v2 v2.32.0 h1:QMILzF8BA7xftE4zVORkRrlv08m12St3csK4RsyRgRs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0/go.mod h1:+ZQ6iUL8+ATyxGDQNuRKpNwJFdGDUi0e7E1QL9MvI7U= +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-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0= +github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= +github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +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/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +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.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= +github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +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/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA= +github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU= +github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/scagogogo/python-requirements-parser v0.0.0-20250717025652-6ca77234c827 h1:+Fu0RVDu7WxwtkELZ9V0PjG+HdxL+ANkGKa6gp45J/g= +github.com/scagogogo/python-requirements-parser v0.0.0-20250717025652-6ca77234c827/go.mod h1:3rCK5dhjXuiTnMXVkO2n9eWZ21qWreUtptCmCT9+rNg= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.2.2 h1:4nc55oVv7nygGnfI9bhLCLzUEs4794y0Bkqx4q2zy7Y= +github.com/shoenig/go-m1cpu v0.2.2/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= +github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= +github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/tree-sitter-grammars/tree-sitter-kotlin v1.1.0 h1:SWIUDASa+WPhDDem1U5IJpYwQEezkqXrUI61OcnORzM= +github.com/tree-sitter-grammars/tree-sitter-kotlin v1.1.0/go.mod h1:eH+flFf3QOa9c9BY9g3Bz02F7zTq30kGIG3cgB0lSlI= +github.com/tree-sitter/go-tree-sitter v0.25.0 h1:sx6kcg8raRFCvc9BnXglke6axya12krCJF5xJ2sftRU= +github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU= +github.com/tree-sitter/tree-sitter-c v0.23.4 h1:nBPH3FV07DzAD7p0GfNvXM+Y7pNIoPenQWBpvM++t4c= +github.com/tree-sitter/tree-sitter-c v0.23.4/go.mod h1:MkI5dOiIpeN94LNjeCp8ljXN/953JCwAby4bClMr6bw= +github.com/tree-sitter/tree-sitter-c-sharp v0.23.5 h1:EeUc2WJE5G1pD6YEqera2yVHYeroeR+/MakKX2a+0xQ= +github.com/tree-sitter/tree-sitter-c-sharp v0.23.5/go.mod h1:H7/aFm5vR1A8Yn5VIOfLWPdlKuJsMgZ5eDmaJdv8bY0= +github.com/tree-sitter/tree-sitter-cpp v0.23.4 h1:LaWZsiqQKvR65yHgKmnaqA+uz6tlDJTJFCyFIeZU/8w= +github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8= +github.com/tree-sitter/tree-sitter-embedded-template v0.23.2 h1:nFkkH6Sbe56EXLmZBqHHcamTpmz3TId97I16EnGy4rg= +github.com/tree-sitter/tree-sitter-embedded-template v0.23.2/go.mod h1:HNPOhN0qF3hWluYLdxWs5WbzP/iE4aaRVPMsdxuzIaQ= +github.com/tree-sitter/tree-sitter-go v0.25.0 h1:cEB0Q3LHgZtS+ECHx9wcP7AwzoOddJFQCVmytX42cVU= +github.com/tree-sitter/tree-sitter-go v0.25.0/go.mod h1:Jrx8QqYN0v7npv1fJRH1AznddllYiCMUChtVjxPK040= +github.com/tree-sitter/tree-sitter-html v0.23.2 h1:1UYDV+Yd05GGRhVnTcbP58GkKLSHHZwVaN+lBZV11Lc= +github.com/tree-sitter/tree-sitter-html v0.23.2/go.mod h1:gpUv/dG3Xl/eebqgeYeFMt+JLOY9cgFinb/Nw08a9og= +github.com/tree-sitter/tree-sitter-java v0.23.5 h1:J9YeMGMwXYlKSP3K4Us8CitC6hjtMjqpeOf2GGo6tig= +github.com/tree-sitter/tree-sitter-java v0.23.5/go.mod h1:NRKlI8+EznxA7t1Yt3xtraPk1Wzqh3GAIC46wxvc320= +github.com/tree-sitter/tree-sitter-javascript v0.25.0 h1:ZkWETb66/w8cc13yhfnNuHOLDQWl3BnKlH6f9AdR88c= +github.com/tree-sitter/tree-sitter-javascript v0.25.0/go.mod h1:lmGD1EJdCA+v0S1u2fFgepMg/opzSg/4pgFym2FPGAs= +github.com/tree-sitter/tree-sitter-json v0.24.8 h1:tV5rMkihgtiOe14a9LHfDY5kzTl5GNUYe6carZBn0fQ= +github.com/tree-sitter/tree-sitter-json v0.24.8/go.mod h1:F351KK0KGvCaYbZ5zxwx/gWWvZhIDl0eMtn+1r+gQbo= +github.com/tree-sitter/tree-sitter-php v0.23.11 h1:iHewsLNDmznh8kgGyfWfujsZxIz1YGbSd2ZTEM0ZiP8= +github.com/tree-sitter/tree-sitter-php v0.23.11/go.mod h1:T/kbfi+UcCywQfUNAJnGTN/fMSUjnwPXA8k4yoIks74= +github.com/tree-sitter/tree-sitter-python v0.25.0 h1:O6XD9v8U1LOcRc3cNj9nM7XufrtEBezE6VrpRrHZDf0= +github.com/tree-sitter/tree-sitter-python v0.25.0/go.mod h1:cpdthSy/Yoa28aJFBscFHlGiU+cnSiSh1kuDVtI8YeM= +github.com/tree-sitter/tree-sitter-ruby v0.23.1 h1:T/NKHUA+iVbHM440hFx+lzVOzS4dV6z8Qw8ai+72bYo= +github.com/tree-sitter/tree-sitter-ruby v0.23.1/go.mod h1:kUS4kCCQloFcdX6sdpr8p6r2rogbM6ZjTox5ZOQy8cA= +github.com/tree-sitter/tree-sitter-rust v0.24.2 h1:NL4nF67ib21RMzzfvkmXlVwe45vvhW10DVyO+D0z/W0= +github.com/tree-sitter/tree-sitter-rust v0.24.2/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI= +github.com/tree-sitter/tree-sitter-typescript v0.23.2 h1:/Odvphn18PniVixb9e97X0DbNVsU6Qocv9mfkyzdXwU= +github.com/tree-sitter/tree-sitter-typescript v0.23.2/go.mod h1:zjzMXT/Ulffel2xfOcAkQQkiAkmgnbtPGlFQw/5X4xA= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74= +github.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +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/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 h1:Tq+E65HaNbZTeqcLw6QUhsV+/EIWdkfUH2CQzcrlE5M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0/go.mod h1:A+EVhDakAj1waJBPhmTszZaSr08ww8MRRKy1nB1rqck= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +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.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +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.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b h1:nB3spRIaSPtxJpxb0k41yljROvm2Jrwfs2pSNdKS5y8= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b/go.mod h1:9N4vtjEwvgQKEdlFVQO0M44ErhecQ9DCSQfGoZ51M6w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 h1:QG6hS4zFplRZvOQedzei4t2pT/zkQc3VR1u+S9/rcAw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/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-20190902080502-41f04d3bba15/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ce12cc6 --- /dev/null +++ b/main.go @@ -0,0 +1,96 @@ +// Binary service-solution-go is the solution specialization of the generic Go +// agent: the `solution-go` service agent. It scaffolds, builds, runs and +// deploys a SOLUTION BACKEND — the entry service of a solution, built on +// github.com/codefly-dev/solution-runtime-go — where the `go` agent scaffolds +// a plain Go service that knows nothing about any of it. +// +// It is a codefly:service agent and nothing else. Services already have a +// protocol (the agent binary, driven by `services.Add`), so there is no core +// change here, no new wire protocol, and no codefly:solution executor: that +// option was weighed and declined in codefly-dev/cli#1215, and +// `codefly add solution --agent solution-go` reaches this binary through the +// path every other service agent is reached through. +// +// Everything that is not solution-specific is the generic Go agent's, imported +// from github.com/codefly-dev/service-go: the Runtime, the Code and Tooling +// servers, and the Builder's Load / Init / Build / Deploy. What this binary +// adds is the Create flow (builder.go, scaffold.go) and the agent's own +// advertisement (service.go). +package main + +import ( + "embed" + + "github.com/codefly-dev/core/agents" + "github.com/codefly-dev/core/builders" + "github.com/codefly-dev/core/resources" + "github.com/codefly-dev/core/shared" + "github.com/codefly-dev/core/toolbox/lang" + + gocode "github.com/codefly-dev/service-go/pkg/code" + goruntime "github.com/codefly-dev/service-go/pkg/runtime" + gotooling "github.com/codefly-dev/service-go/pkg/tooling" +) + +// agent is this agent's identity, read from the manifest beside this file so +// the version is declared once and `codefly publish` moves it in one place. +var agent = shared.Must(resources.LoadFromFs[resources.Agent](shared.Embed(infoFS))) + +// requirements are the files a change to which makes a build stale. A solution +// backend keeps its Go sources at the service root (spec.source-dir: ".", as +// every solution in obin-ai/core-solutions does), so the selector is rooted +// there rather than at a `code/` subdirectory the way the generic Go agent's +// is. +var requirements = builders.NewDependencies(agent.Name, + builders.NewDependency("service.codefly.yaml"), + builders.NewDependency("solution.frontend.codefly.yaml"), + builders.NewDependency(".").WithPathSelect(shared.NewSelect("*.go")), +) + +func main() { + svc := NewService() + + // Code, Tooling and Runtime are inherited wholesale: a solution backend is + // an ordinary Go program to a compiler, a formatter and a process manager, + // and a second copy of any of them here would be a second mechanism for a + // job the generic Go agent already does. + code := gocode.New(svc.Service) + rt := goruntime.New(svc.Service) + tooling := gotooling.New(code, rt) + + agents.Serve(agents.PluginRegistration{ + Agent: svc, + Runtime: rt, + Builder: NewBuilder(svc), + Code: code, + Tooling: tooling, + Toolbox: lang.NewToolboxFromTooling(agent.Name, agent.Version, tooling), + }) +} + +//go:embed agent.codefly.yaml +var infoFS embed.FS + +//go:embed templates/agent +var readmeFS embed.FS + +// factoryFS is the solution backend: what Create writes into the service +// directory. +// +//go:embed all:templates/factory +var factoryFS embed.FS + +// pageFS is the ./Page remote: what Create writes into services/fe-remote, +// beside the backend. `codefly add solution` deliberately does not write it — +// it is an npm package, not a Codefly service — and nothing else does either, +// which is why a solution's page has been hand-copied out of a sibling every +// time (codefly-dev/cli#1217). +// +//go:embed all:templates/page +var pageFS embed.FS + +//go:embed templates/builder +var builderFS embed.FS + +//go:embed templates/deployment +var deploymentFS embed.FS diff --git a/scaffold.go b/scaffold.go new file mode 100644 index 0000000..66f42ff --- /dev/null +++ b/scaffold.go @@ -0,0 +1,252 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/codefly-dev/core/agents/services" + "github.com/codefly-dev/core/shared" + solution "github.com/codefly-dev/solution-runtime-go" +) + +// THE SHAPE THIS AGENT SCAFFOLDS, and the line between it and the CLI. +// +// A solution is one shape: obin-ai/core-solutions' `wiki`, the solution that +// has been running longest, independently confirmed by `proposals`. It is two +// Codefly services and a page: +// +// backend the entry service. This agent scaffolds it. +// web the shell that serves the page where no host shell mounts it. +// A Next.js service; the `nextjs` agent scaffolds it. +// fe-remote the ./Page Module Federation remote. An npm package, not a +// Codefly service, so no `add service` call reaches it — which is +// why this agent writes it, beside the backend, as part of the +// backend's own Create. +// +// WHAT THIS AGENT DOES NOT WRITE, and must not. `codefly add solution` +// (codefly-dev/cli#1216) owns the solution's declarations and writes them +// AFTER this Create returns: +// +// - module-identity, the endpoints, and the only service-dependencies a +// solution backend has (the authority interface, plus `authority/hop` as +// `kind: external`); +// - solution.codefly.yaml, module.contract.codefly.yaml, +// module.package.codefly.yaml and solution.frontend.codefly.yaml. +// +// Writing any of them here would be a second declaration of something the CLI +// already states, and the CLI's would win — silently, by landing second. +// +// In particular NO service-dependency on a consumed module is ever written. +// A solution reaches every module through the authority's hop, which checks +// the call; Codefly renders a service-dependency into a mesh allow rule +// straight to that module's service, PAST the check, which is what +// core-solutions' check-solution-mesh gate refuses (SP-SOL-04). The +// scaffolded consumes_test.go holds the generated solution to that rule in its +// own test suite, so the refusal is local rather than only at the consumer +// repository's gate. + +// scaffoldedService is the service directory this agent scaffolds, and the +// page directory it writes beside it. The names are the shape's, not a +// setting: `codefly add solution` names the same two. +const ( + backendService = "backend" + pageDirectory = "fe-remote" +) + +// scaffold is the template context. It embeds core's own Information so a +// template may reach anything `codefly add service` knows, and adds the four +// values the solution shape derives from the module's name. +type scaffold struct { + *services.Information + + // SolutionID is the solution's one name: the id the host's presence + // document names it by, the alias the authority's edge routes it at + // (/v1/), the principal the authority issues to, and the Codefly + // module name. It is the MODULE's name, never the service's: the service + // is `backend` in every solution. + SolutionID string + // Title is a legible placeholder for the host's shell. The real one is + // solution.frontend.codefly.yaml's, which `codefly add solution --title` + // writes; this is what the page's own heading says until an author + // changes it. + Title string + // Identifier is SolutionID as a JavaScript identifier. Module Federation + // emits a remote's name as one, so a name that cannot become an + // identifier fails inside a bundler — the last hand-scaffolded solution's + // final friction item. newScaffold refuses it here instead. + Identifier string + // RuntimeVersion is the solution runtime the scaffolded go.mod pins, + // stated here so the generated README and GETTING_STARTED name the same + // pin the module requires. + RuntimeVersion string + // InterfaceDescription and OperationPrefix are the two values the + // interface document carries that are derived rather than typed. They are + // in the context, rather than only in the scaffolded openapi.go, because + // this agent renders the FIRST copy of that document itself and the + // scaffolded generator renders every later one: both must read the same + // values or the first `go generate ./...` reports a spurious diff. + InterfaceDescription string + OperationPrefix string +} + +// solutionRuntimeVersion is the solution runtime a freshly scaffolded backend +// pins, and the one templates/factory/go.mod.tmpl requires. +// +// It is a RELEASE, never a pseudo-version: core-solutions' +// check-consumed-apis accepts v0.0.12 or later (or a pseudo-version at or +// after the native projected-token cutover), and a pseudo-version pins a +// commit that may never reach a tag. factory_test.go holds the template to +// this constant and to that floor. +const solutionRuntimeVersion = "v0.0.16" + +// jsIdentifier is what Module Federation can emit a remote's name as. +var jsIdentifier = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`) + +// newScaffold derives the template context from what the agent was asked to +// create, refusing a module name the shape cannot carry. +func newScaffold(info *services.Information) (scaffold, error) { + if info == nil || info.Module == nil || info.Service == nil { + return scaffold{}, fmt.Errorf("the solution-go agent was asked to create a service with no module or service identity") + } + id := info.Module.Name.LowerCase + if strings.TrimSpace(id) == "" { + return scaffold{}, fmt.Errorf("the solution-go agent was asked to create a service in a module with no name") + } + identifier := strings.NewReplacer("-", "_", ".", "_").Replace(id) + if !jsIdentifier.MatchString(identifier) { + return scaffold{}, fmt.Errorf("module name %q does not yield a JavaScript identifier (%q), and Module Federation emits the ./Page remote's name as one: a solution needs a name of lowercase letters, digits and hyphens starting with a letter", id, identifier) + } + return scaffold{ + Information: info, + SolutionID: id, + Title: titleFrom(id), + Identifier: identifier, + RuntimeVersion: solutionRuntimeVersion, + InterfaceDescription: interfaceDescription(id), + OperationPrefix: aliasField(id), + }, nil +} + +// titleFrom is the placeholder title: the module name, hyphens read as spaces, +// each word capitalised. It is the product's name, so it is the author's to +// give — this only keeps the host's shell and the page's heading legible +// instead of showing the raw slug. It is derived exactly as `codefly add +// solution` derives its default --title, so the two agree when neither is +// given one. +func titleFrom(id string) string { + words := strings.FieldsFunc(id, func(r rune) bool { return r == '-' || r == '_' }) + for index, word := range words { + words[index] = strings.ToUpper(word[:1]) + word[1:] + } + return strings.Join(words, " ") +} + +// scaffoldSolution writes the solution backend and its page. +// +// Every tree is written with the factory's own rule — an existing file is +// KEPT, never truncated — so re-running Create over a solution that already +// exists leaves the author's code alone. +func (s *Builder) scaffoldSolution(ctx context.Context, context scaffold) error { + if err := s.Templates(ctx, context, + services.WithFactory(s.factory).WithPathSelect(shared.NewIgnore("go.work*")), + ); err != nil { + return fmt.Errorf("cannot write the solution backend: %w", err) + } + + // The page, beside the backend rather than inside it. `codefly add + // solution` scaffolds `backend` and `web` through their agents and + // deliberately writes no ./Page remote, because a remote is an npm package + // and no `add service` call reaches one — so a solution's page has been + // hand-copied out of a sibling every time. It is written here, from the + // backend's own Create, because the backend is the only part of a solution + // that has an agent at all. + page := filepath.Join(s.pageLocation()) + if err := s.Templates(ctx, context, + services.WithTemplate(s.page, "page", ""). + WithDestination("%s", page). + WithOverride(shared.SkipAll()), + ); err != nil { + return fmt.Errorf("cannot write the solution's ./Page remote at %s: %w", page, err) + } + return nil +} + +// pageLocation is where the ./Page remote lives: services/fe-remote, beside +// this backend. The backend's own location is services/, so the +// remote is its sibling. +func (s *Builder) pageLocation() string { + return filepath.Join(filepath.Dir(s.Location), pageDirectory) +} + +// writeGeneratedConsumption renders consumed_gen.go from the solution +// manifest's api.consumes and writes it into the service directory. It is the +// one file in a scaffolded backend this agent OWNS: `codefly sync` rewrites +// it, and the scaffolded consumes_test.go fails while it and the manifest +// disagree. +func (s *Builder) writeGeneratedConsumption(ctx context.Context) error { + aliases, err := consumedAliases(s.Location) + if err != nil { + return err + } + path := filepath.Join(s.Location, generatedConsumptionFile) + if err := shared.WriteFileAtomic(ctx, path, renderConsumption(aliases), 0o644); err != nil { + return fmt.Errorf("cannot write %s: %w", generatedConsumptionFile, err) + } + return nil +} + +// writeInterfaceArtifact renders the backend's interface document and writes +// it into the service directory. +// +// It is rendered by the solution runtime's own renderer, from a server built +// exactly as the scaffolded main.go builds one — so the bytes this agent +// writes are the bytes the scaffolded `go generate ./...` writes, and the +// scaffolded openapi_test.go passes on a tree nobody has run a generator in +// yet. That document is what `codefly generate contracts` publishes as the +// http endpoint's contract, and the CLI derives the route rows the authority's +// Check applies from it (codefly-dev/cli#1104), so a backend without one +// publishes no contract at all. +// +// The version is passed rather than read: the runtime reads it from +// service.codefly.yaml, which `CreateResponse` has not written yet when this +// runs. +func (s *Builder) writeInterfaceArtifact(ctx context.Context, context scaffold, version string) error { + server := solution.New(solution.Manifest{ID: context.SolutionID, Contract: context.SolutionID}) + document, err := server.InterfaceArtifact("", solution.InterfaceInfo{ + Title: context.SolutionID + " solution backend", + Version: version, + Description: context.InterfaceDescription, + OperationPrefix: context.OperationPrefix, + Tag: context.SolutionID, + }) + if err != nil { + return fmt.Errorf("cannot render the backend's interface document: %w", err) + } + path := filepath.Join(s.Location, filepath.FromSlash(interfaceArtifactPath)) + if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { + return fmt.Errorf("cannot create %s: %w", filepath.Dir(path), err) + } + if err := shared.WriteFileAtomic(ctx, path, document, 0o644); err != nil { + return fmt.Errorf("cannot write %s: %w", interfaceArtifactPath, err) + } + return nil +} + +// interfaceArtifactPath is where a solution backend commits its interface +// document, as wiki and proposals both do. The scaffolded openapi.go names the +// same path. +const interfaceArtifactPath = "openapi/api.swagger.json" + +// interfaceDescription is the document's own description. It is written once +// here and rendered into the scaffolded openapi.go, so the agent's render and +// the backend's own render produce identical bytes. +func interfaceDescription(id string) string { + return "The " + id + " solution's backend under the authority interface, at the route the authority's edge forwards it on: " + + "the routes it serves itself, and the module calls the solution runtime relays as the viewer. " + + "A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, " + + "and the module's own method policy, gate it." +} diff --git a/scaffold_test.go b/scaffold_test.go new file mode 100644 index 0000000..bff96d8 --- /dev/null +++ b/scaffold_test.go @@ -0,0 +1,280 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/codefly-dev/core/agents/services" + "github.com/codefly-dev/core/resources" + "github.com/stretchr/testify/require" +) + +// scaffoldInto drives everything Create writes, into a throwaway solution laid +// out the way `codefly add solution` lays one out: +// +// /solution.codefly.yaml (only when manifest is non-empty) +// /services/backend the backend this agent scaffolds +// /services/fe-remote the ./Page remote it writes beside it +// +// It stops short of CreateResponse, which saves the service manifest through +// core and needs a loaded Builder: what is under test here is the tree. +func scaffoldInto(t *testing.T, module, manifest string) (root string) { + t.Helper() + root = t.TempDir() + if manifest != "" { + require.NoError(t, os.WriteFile(filepath.Join(root, "solution.codefly.yaml"), []byte(manifest), 0o600)) + } + location := filepath.Join(root, "services", backendService) + require.NoError(t, os.MkdirAll(location, 0o750)) + + svc := NewService() + identity := &resources.ServiceIdentity{Name: backendService, Module: module, Workspace: "workspace"} + svc.Base.Location = location + svc.Base.Identity = identity + svc.Base.Information = &services.Information{ + Service: resources.ToServiceWithCase(identity), + Module: resources.ToModuleWithCase(identity), + Agent: agent, + } + + builder := NewBuilder(svc) + ctx := context.Background() + context, err := newScaffold(svc.Base.Information) + require.NoError(t, err) + require.NoError(t, builder.scaffoldSolution(ctx, context)) + require.NoError(t, builder.writeGeneratedConsumption(ctx)) + require.NoError(t, builder.writeInterfaceArtifact(ctx, context, "0.0.0")) + return root +} + +// emptyManifest is a solution manifest that consumes nothing — what `codefly +// add solution` writes with no --consumes. +const emptyManifest = `schema_version: codefly.solution-manifest/v0 +protocol_version: codefly.solution/v0 +api: + exposes: + - id: http + protocol: http +lifecycle: + package: true + render: true +` + +// consumingManifest declares two modules, as `codefly add solution --consumes` +// writes them: route names only, no scopes — authority is declared once, in +// the module contract. +const consumingManifest = `schema_version: codefly.solution-manifest/v0 +protocol_version: codefly.solution/v0 +api: + exposes: + - id: http + protocol: http + consumes: + - id: documents + protocol: connect + module: documents + service: documents + endpoint: grpc + as: documents + - id: record-store + protocol: connect + module: records + service: records + endpoint: connect + as: record-store +lifecycle: + package: true + render: true +` + +func TestCreateWritesTheBackendAndThePageBesideIt(t *testing.T) { + root := scaffoldInto(t, "notes", emptyManifest) + backend := filepath.Join(root, "services", backendService) + page := filepath.Join(root, "services", pageDirectory) + + for _, name := range []string{ + "main.go", "operations.go", "consumes.go", "consumes_test.go", + generatedConsumptionFile, "openapi.go", "openapi_test.go", + filepath.FromSlash(interfaceArtifactPath), + "go.mod", "go.sum", "GETTING_STARTED.md", ".gitignore", + } { + require.FileExistsf(t, filepath.Join(backend, name), "the backend is missing %s", name) + } + // The ./Page remote: an npm package, beside the backend, which `codefly add + // solution` deliberately does not write and no `add service` call reaches. + for _, name := range []string{ + "package.json", "rsbuild.config.ts", "tsconfig.json", "vitest.config.ts", + ".npmrc", "README.md", + filepath.Join("src", "Page.tsx"), filepath.Join("src", "Page.test.tsx"), + filepath.Join("src", "binding.ts"), filepath.Join("src", "page.css"), + } { + require.FileExistsf(t, filepath.Join(page, name), "the ./Page remote is missing %s", name) + } +} + +// TestTheSolutionIdIsTheModuleName pins what a solution's one name is. It is +// the MODULE's, not the service's: the service is `backend` in every solution, +// and a backend that called itself `backend` to the host would be routed at +// /v1/backend and named that in every presence document. +func TestTheSolutionIdIsTheModuleName(t *testing.T) { + root := scaffoldInto(t, "notes", emptyManifest) + main := read(t, filepath.Join(root, "services", backendService, "main.go")) + require.Contains(t, main, `const solutionID = "notes"`) + + page := read(t, filepath.Join(root, "services", pageDirectory, "package.json")) + var declared struct { + Name string `json:"name"` + Exports map[string]string `json:"exports"` + } + require.NoError(t, json.Unmarshal([]byte(page), &declared), "the page's package.json must be valid JSON") + require.Equal(t, "notes-fe-remote", declared.Name) + require.Equal(t, "./src/Page.tsx", declared.Exports["./Page"], "a host federates ./Page") +} + +// TestAModuleNameThatIsNoJavaScriptIdentifierIsRefused: Module Federation +// emits a remote's name as a JavaScript global, so a name that cannot become +// an identifier fails inside a bundler, long after the scaffold. It is refused +// here instead, by name. +func TestAModuleNameThatIsNoJavaScriptIdentifierIsRefused(t *testing.T) { + identity := &resources.ServiceIdentity{Name: backendService, Module: "9lives", Workspace: "workspace"} + _, err := newScaffold(&services.Information{ + Service: resources.ToServiceWithCase(identity), + Module: resources.ToModuleWithCase(identity), + Agent: agent, + }) + require.ErrorContains(t, err, "JavaScript identifier") +} + +// TestTheRemoteNameIsAnIdentifier: a hyphenated module name is legal Codefly +// and illegal JavaScript, so the remote's name carries underscores while +// everything else keeps the hyphens. +func TestTheRemoteNameIsAnIdentifier(t *testing.T) { + root := scaffoldInto(t, "client-notes", emptyManifest) + config := read(t, filepath.Join(root, "services", pageDirectory, "rsbuild.config.ts")) + require.Contains(t, config, `const name = "client_notes"`) + require.Contains(t, config, `"./Page": "./src/Page.tsx"`) +} + +// TestTheGeneratedConsumptionIsTheManifests holds the one rule that made this +// agent worth building: consumption is declared ONCE, in api.consumes, and the +// Go half is generated from it. +func TestTheGeneratedConsumptionIsTheManifests(t *testing.T) { + root := scaffoldInto(t, "notes", consumingManifest) + generated := read(t, filepath.Join(root, "services", backendService, generatedConsumptionFile)) + + require.Contains(t, generated, "DO NOT EDIT") + require.Contains(t, generated, `Documents string`) + require.Contains(t, generated, `RecordStore string`) + require.Contains(t, generated, `var consumedAsAll = []string{"documents", "record-store"}`) +} + +// TestAMissingManifestIsAnEmptyConsumption is the ordering `codefly add +// solution` imposes: it scaffolds the services and writes the documents +// afterwards, so at Create time there is no manifest to read and the honest +// answer is "nothing so far". `codefly sync` picks it up. +func TestAMissingManifestIsAnEmptyConsumption(t *testing.T) { + root := scaffoldInto(t, "notes", "") + generated := read(t, filepath.Join(root, "services", backendService, generatedConsumptionFile)) + require.Contains(t, generated, "var consumedAsAll = []string{}") + require.Contains(t, generated, "consumes no module yet") + require.NotContains(t, generated, "consumedAs = struct") +} + +// TestSyncRewritesTheGeneratedConsumption: the generated file goes stale on a +// change to a document the CLI owns rather than to any Go source, so no build +// would notice. `codefly sync` is what moves it, and this is that path. +func TestSyncRewritesTheGeneratedConsumption(t *testing.T) { + root := scaffoldInto(t, "notes", "") + backend := filepath.Join(root, "services", backendService) + require.Contains(t, read(t, filepath.Join(backend, generatedConsumptionFile)), "[]string{}") + + require.NoError(t, os.WriteFile(filepath.Join(root, "solution.codefly.yaml"), []byte(consumingManifest), 0o600)) + + svc := NewService() + identity := &resources.ServiceIdentity{Name: backendService, Module: "notes", Workspace: "workspace"} + svc.Base.Location = backend + svc.Base.Identity = identity + svc.Base.Information = &services.Information{ + Service: resources.ToServiceWithCase(identity), + Module: resources.ToModuleWithCase(identity), + Agent: agent, + } + require.NoError(t, NewBuilder(svc).writeGeneratedConsumption(context.Background())) + require.Contains(t, read(t, filepath.Join(backend, generatedConsumptionFile)), + `var consumedAsAll = []string{"documents", "record-store"}`) +} + +// TestScaffoldingKeepsWhatIsAlreadyThere: Create writes with the factory's own +// rule — an existing file is kept, never truncated — so re-running it over a +// solution somebody has worked in leaves their code alone. +func TestScaffoldingKeepsWhatIsAlreadyThere(t *testing.T) { + root := scaffoldInto(t, "notes", emptyManifest) + backend := filepath.Join(root, "services", backendService) + page := filepath.Join(root, "services", pageDirectory) + authored := "package main\n\n// mine\n" + require.NoError(t, os.WriteFile(filepath.Join(backend, "operations.go"), []byte(authored), 0o600)) + authoredPage := "// mine\n" + require.NoError(t, os.WriteFile(filepath.Join(page, "src", "Page.tsx"), []byte(authoredPage), 0o600)) + + svc := NewService() + identity := &resources.ServiceIdentity{Name: backendService, Module: "notes", Workspace: "workspace"} + svc.Base.Location = backend + svc.Base.Identity = identity + svc.Base.Information = &services.Information{ + Service: resources.ToServiceWithCase(identity), + Module: resources.ToModuleWithCase(identity), + Agent: agent, + } + builder := NewBuilder(svc) + rendered, err := newScaffold(svc.Base.Information) + require.NoError(t, err) + require.NoError(t, builder.scaffoldSolution(context.Background(), rendered)) + + require.Equal(t, authored, read(t, filepath.Join(backend, "operations.go"))) + require.Equal(t, authoredPage, read(t, filepath.Join(page, "src", "Page.tsx"))) +} + +// TestTheInterfaceDocumentIsRenderedByTheRuntime: the document is what +// `codefly generate contracts` publishes as the http endpoint's contract, and +// the CLI derives the rows the authority's Check applies from it, so a backend +// without one publishes no contract at all. The agent renders the first copy +// with the runtime's own renderer, so the committed bytes are the bytes the +// scaffolded `go generate ./...` writes — openapi_test.go passes on a tree +// nobody has run a generator in yet. +func TestTheInterfaceDocumentIsRenderedByTheRuntime(t *testing.T) { + root := scaffoldInto(t, "notes", emptyManifest) + document := read(t, filepath.Join(root, "services", backendService, filepath.FromSlash(interfaceArtifactPath))) + + var declared struct { + Swagger string `json:"swagger"` + Info struct { + Title string `json:"title"` + Version string `json:"version"` + Description string `json:"description"` + } `json:"info"` + } + require.NoError(t, json.Unmarshal([]byte(document), &declared)) + require.Equal(t, "2.0", declared.Swagger) + require.Equal(t, "notes solution backend", declared.Info.Title) + require.Equal(t, "0.0.0", declared.Info.Version) + require.Equal(t, interfaceDescription("notes"), declared.Info.Description) + + // The scaffolded generator must ask for the same document, or the first + // `go generate ./...` reports a diff nobody introduced. + generator := read(t, filepath.Join(root, "services", backendService, "openapi.go")) + require.Contains(t, generator, `Title: "notes solution backend"`) + require.Contains(t, generator, interfaceDescription("notes")) + require.Contains(t, generator, `OperationPrefix: "Notes"`) + require.Contains(t, generator, interfaceArtifactPath) +} + +func read(t *testing.T, path string) string { + t.Helper() + content, err := os.ReadFile(path) //nolint:gosec // a path this test just wrote + require.NoError(t, err) + return strings.ReplaceAll(string(content), "\r\n", "\n") +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..eb2565b --- /dev/null +++ b/service.go @@ -0,0 +1,110 @@ +package main + +import ( + "context" + + "github.com/codefly-dev/core/agents/contract" + "github.com/codefly-dev/core/agents/services" + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" + "github.com/codefly-dev/core/languages" + runnersbase "github.com/codefly-dev/core/runners/base" + "github.com/codefly-dev/core/shared" + "github.com/codefly-dev/core/templates" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + goservice "github.com/codefly-dev/service-go/pkg/service" +) + +// Settings is this agent's service configuration. It is the generic Go agent's +// settings and nothing more: a solution backend has no toggle of its own, +// because everything that distinguishes one is DECLARED rather than +// configured — what it is in solution.codefly.yaml, what it asks for in +// module.contract.codefly.yaml, and what it consumes in api.consumes. +// +// `yaml:",inline"` keeps the shape flat, so a solution backend's +// service.codefly.yaml `spec:` reads exactly like any Go service's +// (hot-reload, debug-symbols, source-dir, …). +type Settings struct { + goservice.Settings `yaml:",inline"` +} + +// Service is the solution specialization. It embeds *goservice.Service to +// inherit the identity, the logger, the location and source resolution, and +// overrides only the advertisement. +type Service struct { + *goservice.Service + + Settings *Settings +} + +// NewService builds the agent's service state with the solution backend's own +// source convention: the Go module at the SERVICE ROOT, not under `code/`. +// +// That is not a preference. A solution backend embeds +// solution.frontend.codefly.yaml, which lives beside service.codefly.yaml +// because the CLI reads the same file for the presence document it signs +// (pkg/solutionrun) — and //go:embed cannot reach up out of its own directory. +// A module under `code/` therefore could not embed the declaration it serves, +// and the served manifest and the signed presence would become two files. +func NewService() *Service { + generic := goservice.New(agent) + settings := &Settings{} + settings.SourceDir = solutionSourceDir + generic.Settings = &settings.Settings + generic.InputPlans = goservice.GenericGoInputPlans + return &Service{Service: generic, Settings: settings} +} + +// solutionSourceDir is where a solution backend's Go module lives, relative to +// the service directory: the service root. See NewService. +const solutionSourceDir = "." + +// GetAgentInformation is this agent's advertisement. It differs from the +// generic Go agent's in two ways that a caller acts on: it serves HTTP (the +// one endpoint a solution backend exposes, which the authority's edge fronts), +// and it carries this repository's own README so `codefly add service` prints +// what a solution backend is rather than "Generic Go service". +func (s *Service) GetAgentInformation(ctx context.Context, _ *agentv0.AgentInformationRequest) (*agentv0.AgentInformation, error) { + defer s.Wool.Catch() + + info := s.Information + if info == nil { + info = &services.Information{} + } + readme, err := templates.ApplyTemplateFrom(ctx, shared.Embed(readmeFS), "templates/agent/README.md", info) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + advertisement := services.Advertisement{ + Backends: runnersbase.BackendSupport{ + Local: func() bool { return languages.HasGoRuntime(nil) }, + Nix: true, + Docker: true, + }, + Toolchains: []agentv0.Toolchain_Type{agentv0.Toolchain_GO}, + Languages: []agentv0.Language_Type{agentv0.Language_GO}, + // A solution backend serves one HTTP endpoint and the authority's edge + // fronts it; it speaks no gRPC of its own, and the module calls it + // relays are the runtime's gRPC client, not a served protocol. + Protocols: []agentv0.Protocol_Type{agentv0.Protocol_HTTP}, + ReadMe: readme, + Validation: goservice.ValidationCapabilities(), + // Sync is supported: it regenerates what api.consumes projects into Go + // and re-renders the interface document (builder.go). + EffectiveInputsVersions: []uint32{1}, + }.Build() + advertisement.Validation.Sync.Supported = true + // The deployment path is core's DeployKustomize, which judges dependency + // edges with the request's composition provenance on this core; the host + // requires the live advertisement, not the linked core version. + advertisement.Contract = contract.Current() + advertisement.Contract.Capabilities = append(advertisement.Contract.Capabilities, + contract.DeploymentCompositionProvenance, + // The inherited Runtime reports what it started on every native start, + // which is the only thing that earns this capability. + contract.RuntimeStartedArtifact, + ) + return advertisement, nil +} diff --git a/shape_test.go b/shape_test.go new file mode 100644 index 0000000..a45312d --- /dev/null +++ b/shape_test.go @@ -0,0 +1,167 @@ +package main + +import ( + "io/fs" + "path" + "regexp" + "strings" + "testing" + + "github.com/codefly-dev/core/composition" + modulecontract "github.com/codefly-dev/core/contracts/module" + "github.com/codefly-dev/core/solution/manifest" + "github.com/stretchr/testify/require" +) + +// THE LINE BETWEEN THIS AGENT AND THE CLI, held by tests rather than by a +// comment. +// +// `codefly add solution` (codefly-dev/cli#1216) writes the solution's +// declarations — the five documents, module-identity, the endpoints, and the +// only service-dependencies a solution backend has — and it writes them AFTER +// this agent's Create returns. Anything this agent wrote in those places would +// be overwritten silently, by landing first, and a reader would have two +// sources for one statement. +// +// These tests read the TEMPLATES, not a scaffolded tree, so a line added to a +// template fails here whether or not any test happens to scaffold it. + +// templateText is every factory and page template, by its path inside the +// embedded tree. +func templateText(t *testing.T) map[string]string { + t.Helper() + all := map[string]string{} + for _, tree := range []struct { + files fs.FS + root string + }{{factoryFS, "templates/factory"}, {pageFS, "templates/page"}} { + require.NoError(t, fs.WalkDir(tree.files, tree.root, func(name string, entry fs.DirEntry, err error) error { + if err != nil || entry.IsDir() { + return err + } + content, readErr := fs.ReadFile(tree.files, name) + if readErr != nil { + return readErr + } + all[name] = string(content) + return nil + })) + } + require.NotEmpty(t, all) + return all +} + +// TestNoTemplateIsOneOfTheSolutionsDocuments. The five documents a solution is +// installed from are the CLI's: solution.codefly.yaml, +// module.contract.codefly.yaml, module.package.codefly.yaml, +// solution.frontend.codefly.yaml and the module manifest. This agent reads the +// first one and embeds the fourth; it writes none of them. +func TestNoTemplateIsOneOfTheSolutionsDocuments(t *testing.T) { + owned := []string{ + manifest.FileName, + modulecontract.FileName, + composition.PackageManifestFileName, + "solution.frontend.codefly.yaml", + "module.codefly.yaml", + } + for name := range templateText(t) { + rendered := strings.TrimSuffix(path.Base(name), ".tmpl") + for _, document := range owned { + require.NotEqualf(t, document, rendered, + "%s would write %s, which `codefly add solution` owns and writes after this Create returns", name, document) + } + } +} + +// TestNoTemplateDeclaresWhatTheServiceManifestDeclares. A solution backend's +// manifest carries module-identity, one endpoint with the /health readiness +// probe, and exactly two service-dependencies (the authority interface and its +// hop). All of it is the CLI's, so no template may state any of it — not even +// as a value that happens to agree today. +func TestNoTemplateDeclaresWhatTheServiceManifestDeclares(t *testing.T) { + // Matched at the start of a line, so prose and a commented example in + // GETTING_STARTED.md — which deliberately shows the block an author adds + // by hand when the service was created outside `add solution` — is not a + // declaration. A template that is itself a manifest would declare them at + // column zero. + declarations := []*regexp.Regexp{ + regexp.MustCompile(`(?m)^module-identity:`), + regexp.MustCompile(`(?m)^service-dependencies:`), + regexp.MustCompile(`(?m)^endpoints:`), + regexp.MustCompile(`(?m)^workspace-configuration-dependencies:`), + } + for name, content := range templateText(t) { + for _, declaration := range declarations { + require.Falsef(t, declaration.MatchString(content), + "%s declares %s, which `codefly add solution` writes after this Create returns", name, declaration) + } + } +} + +// TestNoTemplateNamesAModuleEdge is SP-SOL-04 at the source. A solution +// reaches every module through the authority's hop, which CHECKS the call; +// Codefly renders a `service-dependencies` edge into a mesh allow rule +// straight to that module's service, past the check, and +// obin-ai/core-solutions' check-solution-mesh gate refuses it. +// +// The scaffolded consumes_test.go holds a generated solution to the same rule +// in its own suite. This holds the agent to never writing one. +func TestNoTemplateNamesAModuleEdge(t *testing.T) { + edge := regexp.MustCompile(`(?m)^\s*-\s+name:\s+\S+\s*$`) + for name, content := range templateText(t) { + if !strings.Contains(content, "service-dependencies") { + continue + } + // GETTING_STARTED.md is the one template that shows the block, inside + // a fenced example for a service created outside `add solution`. It + // may show the host's own door and nothing else. + require.Equal(t, "templates/factory/GETTING_STARTED.md.tmpl", name, + "%s mentions service-dependencies; only GETTING_STARTED may, as the example an author copies", name) + for _, line := range edge.FindAllString(content, -1) { + require.Containsf(t, line, "hop", + "%s shows a service-dependency on something other than the authority's hop: %q", name, strings.TrimSpace(line)) + } + } +} + +// TestNoTemplateCarriesProductContent. This agent is published from +// codefly-dev and scaffolds a solution for anyone. The template it was modelled +// on (obin-ai/core-solutions' templates/solution) depends on that +// repository's own vendored kits and shared packages by relative path, which +// is exactly why it could not leave the repository. None of it comes along. +func TestNoTemplateCarriesProductContent(t *testing.T) { + for name, content := range templateText(t) { + require.NotContainsf(t, content, "@obin-ai/", "%s depends on a product package", name) + require.NotContainsf(t, content, "obin-ai/", "%s names a product repository as a dependency", name) + require.NotContainsf(t, content, `"file:`, "%s depends on a vendored archive by path, which only resolves inside one repository", name) + require.NotContainsf(t, content, "../../../", "%s reaches outside the solution it scaffolds", name) + } +} + +// TestThePageSharesEveryHostSingleton. Each entry in the Module Federation +// shared policy must be the HOST's single instance, or its React context and +// its skin tokens do not line up across the federation boundary: the kit +// renders unstyled, stale, or — for react-dom/client — not at all (React error +// #527, a page that never mounts). +// +// A missing entry is invisible until a browser loads the remote inside a real +// host, so it is pinned here. +func TestThePageSharesEveryHostSingleton(t *testing.T) { + config := templateText(t)["templates/page/rsbuild.config.ts.tmpl"] + require.NotEmpty(t, config) + for _, shared := range []string{ + "react", "react/jsx-runtime", "react-dom", "react-dom/client", + "@codefly-dev/ui", "@codefly-dev/saas-ui", "@codefly-dev/saas-sdk", + "@codefly-dev/saas-plugin-contract", "@codefly-dev/saas-plugin-contract/capabilities", + "@codefly-dev/saas-plugin-react", "@codefly-dev/saas-plugin-react/runtime", + "@codefly-dev/saas-plugin-react/ui", + } { + require.Containsf(t, config, `"`+shared+`"`, "the shared policy does not seal %s to the host's instance", shared) + } + // "auto" lets the container resolve its chunk URLs relative to wherever its + // remoteEntry was loaded from, so one build is servable from any origin. + require.Contains(t, config, `process.env.ASSET_PREFIX ?? "auto"`) + // A host's CSP refuses a cross-origin stylesheet, so the remote's CSS must + // be inlined or the page fails to mount. + require.Contains(t, config, "injectStyles: true") +} diff --git a/templates/agent/README.md.tmpl b/templates/agent/README.md.tmpl new file mode 100644 index 0000000..f1f3ff3 --- /dev/null +++ b/templates/agent/README.md.tmpl @@ -0,0 +1,26 @@ +# {{ .Service.Name.Title }} — a solution backend + +This service is the entry service of the `{{ .Module.Name.LowerCase }}` solution: the +workload a host installs, built on +[`solution-runtime-go`](https://github.com/codefly-dev/solution-runtime-go). + +The runtime does everything a solution needs identically — it presents what the +solution is, answers `/health`, serves the page's calls, and relays the page's +module calls to the authority's hop **as the viewer**, where each module +authorizes the call. What lives here is this solution's own composition: + +| file | what it says | +|---|---| +| `main.go` | what the solution is: its id, its contract, its capabilities | +| `operations.go` | the routes this backend serves itself, each with the policy a call must hold | +| `consumes.go` | per consumed module: the authority asked for the viewer, and the methods that may be called | +| `consumed_gen.go` | **generated** from `api.consumes` — `codefly sync` rewrites it | +| `openapi/api.swagger.json` | **generated** from the routes above — `go generate ./...` rewrites it | + +A solution reaches every module through the authority's hop, which checks the +call. It therefore declares **no** `service-dependencies` edge on a module it +consumes: Codefly renders such an edge into a mesh allow rule straight to that +module's service, past the check. `consumes_test.go` holds this service to that. + +The page this solution serves is `../fe-remote` — the `./Page` Module +Federation remote, an npm package rather than a Codefly service. diff --git a/templates/builder/Dockerfile.tmpl b/templates/builder/Dockerfile.tmpl new file mode 100644 index 0000000..ef9a629 --- /dev/null +++ b/templates/builder/Dockerfile.tmpl @@ -0,0 +1,114 @@ +# syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32 +# Base images are pinned by tag AND digest, and the pair is what makes this +# recipe reproducible: the digest names the exact bytes, the tag stays readable +# and keeps the image inside Dependabot's reach. Dependabot's Docker updater +# matches files by /dockerfile|containerfile/i, so it fetches this .tmpl, and +# .github/dependabot.yml already points its docker ecosystem at +# /templates/builder with no `update-types` filter — the filter only recognises +# semver bumps and would drop a digest update out of the group. It rewrites tag +# and digest together, weekly. To refresh out of cycle run +# .github/scripts/refresh-base-digests.sh; never hand-edit a digest. +# +# Both digests are OCI image-INDEX digests, not per-platform manifests, so the +# two platforms pkg/builder declares (linux/amd64, linux/arm64) still resolve +# through the pin. +# +# The Go modules the recipe declares it downloads (go_module_downloads), fetched +# by the caller on its own machine before any image build and supplied here as +# the build context of this name, in the Go module proxy layout. Without a +# caller supplying it, the stage is empty and the download below uses the +# network as it always did. +FROM scratch AS gomodproxy + +# Build stage +# +# The Go tag names the exact patch release, never a floating `1.27`. The +# official golang image sets GOTOOLCHAIN=local, so the Go in the image is the +# only Go that builds: a service whose go.mod requires a newer patch is refused +# outright, and with the caller's prefetched modules as the only module source +# there is no toolchain to download either. A floating minor tag hid which +# patch the digest held, and the pin sat on 1.27.1 while the fleet's modules +# moved to `go 1.27.3` — the tag now says which patch this is, so a stale one is +# readable in the reference rather than only in a failed build. +# +# It is the same image pkg/runtime/runtime-image.json locks the Go runtime +# container to, so a module builds the same way in a local container run and in +# this recipe. Moving Go moves both; dockerfile_template_test.go holds that. +FROM --platform=$BUILDPLATFORM golang:1.27.3-alpine3.23@sha256:895e5feab11a4b3774d24ee3c9579028456b82ad37f42a9eb6f82adaf776a201 AS builder + +# git is the one package the pinned base lacks and GOPRIVATE module fetches +# need; ca-certificates is already in it, so installing it again would add a +# floating input for nothing. Alpine's index serves only the current -rN of +# each package, so `git=` stops resolving the day 3.24 ships a patch +# and every rebuild of an already-published recipe fails — a version pin here +# buys reproducibility by trading away the ability to rebuild at all. The +# resolved set is recorded instead, and travels with the image. +RUN apk add --no-cache git && apk info -v | sort > /apk-builder.txt + +WORKDIR /app + +COPY code/ ./code/ +WORKDIR /app/code +ARG TARGETOS +ARG TARGETARCH + +# Private Go modules. The recipe declares its module download, and the caller +# fetches it before the build and supplies it as the gomodproxy context above: +# the build reads the modules from there and needs no credential. GOPRIVATE, a +# build argument the CLI sets from the host's own, only matters when nothing was +# supplied — a plain `docker build` — where those module paths bypass the public +# proxy and checksum database and are fetched with git, with the optional netrc +# secret below. The credential is never a build argument, an ENV or a layer. +ARG GOPRIVATE + +# With the caller's prefetched modules, they are the only module source: no +# credential is needed or mounted, a module missing from them fails here rather +# than being fetched, and go.sum alone verifies them (the caller's fetch was +# verified against the checksum database on its side). GOPRIVATE is cleared so +# private paths are read from the proxy like any other. +# +# `go version -m` records what actually produced the binary: the toolchain and +# every module version. The base digest fixes the toolchain shipped in the +# image and the official golang image sets GOTOOLCHAIN=local, so that toolchain +# is the one that builds and no substitution is possible — a service whose +# go.mod requires a newer patch fails here, naming both versions, rather than +# quietly building with something the digest does not pin. Recording it keeps +# the pair readable from the artifact. +RUN --mount=type=bind,from=gomodproxy,target=/gomodproxy \ + --mount=type=secret,id=netrc,target=/root/.netrc,required=false \ + if [ -n "$(ls -A /gomodproxy)" ]; then export GOPROXY=file:///gomodproxy GOSUMDB=off GOPRIVATE= GONOPROXY= GONOSUMDB=; fi && \ + go mod download && go mod verify && CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -ldflags='-w -s -extldflags "-static"' -o app && \ + go version -m app > /go-build-info.txt + +# Final stage +FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 + +# No `apk add` here on purpose. The pinned base already ships +# ca-certificates-bundle, which owns /etc/ssl/certs/ca-certificates.crt — the +# first path crypto/x509 reads on Linux, and all a static binary needs to verify +# TLS. Installing the full ca-certificates package on top would only add +# update-ca-certificates and c_rehash, which nothing here runs, in exchange for +# an OS-package input the digest does not cover. With that gone the runtime +# image's package set is determined entirely by the digest above: reproducible, +# not merely recorded. +# +# What is merely recorded is the builder stage, which does install a package. +# Its resolved set and the Go build info come across here so the inputs that +# produced this artifact are readable from the artifact. The runtime set is +# written too: it is fixed by the digest, so two builds that disagree on it are +# evidence the pin stopped holding. +COPY --from=builder /apk-builder.txt /go-build-info.txt /etc/codefly/build-inputs/ +RUN apk info -v | sort > /etc/codefly/build-inputs/apk-runtime.txt + +WORKDIR /app + +{{ range .Envs }} +ENV {{ .Key }}={{ .Value }} +{{ end }} + +RUN adduser -D appuser +COPY --chown=appuser --from=builder /app/code/app . +USER appuser + +EXPOSE 8080 +CMD ["./app"] diff --git a/templates/builder/dockerignore.tmpl b/templates/builder/dockerignore.tmpl new file mode 100644 index 0000000..d7b6bd1 --- /dev/null +++ b/templates/builder/dockerignore.tmpl @@ -0,0 +1,2 @@ +code/go.work +code/go.work.sum diff --git a/templates/deployment/kustomize/base/deployment.yaml.tmpl b/templates/deployment/kustomize/base/deployment.yaml.tmpl new file mode 100644 index 0000000..2a4f26d --- /dev/null +++ b/templates/deployment/kustomize/base/deployment.yaml.tmpl @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Service.Name.DNSCase }} + namespace: {{ .Namespace }} +spec: + replicas: {{ .Replicas }} + selector: + matchLabels: + app: {{ .Service.Name.DNSCase }} + template: + metadata: + labels: + app: {{ .Service.Name.DNSCase }} + sha: "{{ .Sha }}" + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 30 + # The image runs as a named user (appuser), and the kubelet cannot prove a + # name is non-root, so runAsNonRoot alone refuses to start the container. + # A numeric uid can be verified. The static binary runs on alpine, whose + # nobody is 65534, the same identity the go-grpc agent pins. + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + fsGroup: 65534 + seccompProfile: + type: RuntimeDefault + containers: + - name: {{ .Service.Name.DNSCase }} + image: {{ .Image }} + ports: + - name: http + containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi + startupProbe: + tcpSocket: + port: 8080 + readinessProbe: + tcpSocket: + port: 8080 + livenessProbe: + tcpSocket: + port: 8080 + envFrom: + - configMapRef: + name: cm-{{ .Service.Name.DNSCase }} +{{- if not .Restricted }} + - secretRef: + name: secret-{{ .Service.Name.DNSCase }} +{{- end }} +{{- if and .Restricted .SecretReferences }} + env: +{{- range $environmentVariable, $reference := .SecretReferences }} + - name: {{ $environmentVariable }} + valueFrom: + secretKeyRef: + name: {{ $reference.Name }} + key: {{ $reference.Key }} + optional: {{ $reference.Optional }} +{{- end }} +{{- end }} diff --git a/templates/deployment/kustomize/base/kustomization.yaml.tmpl b/templates/deployment/kustomize/base/kustomization.yaml.tmpl new file mode 100644 index 0000000..fa0d970 --- /dev/null +++ b/templates/deployment/kustomize/base/kustomization.yaml.tmpl @@ -0,0 +1,6 @@ +resources: +{{- if not .Restricted }} + - namespace.yaml +{{- end }} + - deployment.yaml + - service.yaml diff --git a/templates/deployment/kustomize/base/namespace.yaml.tmpl b/templates/deployment/kustomize/base/namespace.yaml.tmpl new file mode 100644 index 0000000..ea32b3e --- /dev/null +++ b/templates/deployment/kustomize/base/namespace.yaml.tmpl @@ -0,0 +1,8 @@ +{{- if not .Restricted }} +apiVersion: v1 +kind: Namespace +metadata: + name: "{{ .Namespace }}" + labels: + app.kubernetes.io/managed-by: codefly +{{- end }} diff --git a/templates/deployment/kustomize/base/service.yaml.tmpl b/templates/deployment/kustomize/base/service.yaml.tmpl new file mode 100644 index 0000000..9684cec --- /dev/null +++ b/templates/deployment/kustomize/base/service.yaml.tmpl @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Service.Name.DNSCase }} + namespace: {{ .Namespace }} +spec: + selector: + app: {{ .Service.Name.DNSCase }} + ports: + - protocol: TCP + name: http + port: 8080 + targetPort: 8080 diff --git a/templates/deployment/kustomize/overlays/environment/configmap.yaml.tmpl b/templates/deployment/kustomize/overlays/environment/configmap.yaml.tmpl new file mode 100644 index 0000000..10079eb --- /dev/null +++ b/templates/deployment/kustomize/overlays/environment/configmap.yaml.tmpl @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm-{{ .Service.Name.DNSCase }} + namespace: "{{ .Namespace }}" +data: +{{- range $key, $value := .Deployment.ConfigMap }} + {{ $key }}: {{ printf "%q" $value }} +{{- end }} diff --git a/templates/deployment/kustomize/overlays/environment/kustomization.yaml.tmpl b/templates/deployment/kustomize/overlays/environment/kustomization.yaml.tmpl new file mode 100644 index 0000000..0790a63 --- /dev/null +++ b/templates/deployment/kustomize/overlays/environment/kustomization.yaml.tmpl @@ -0,0 +1,6 @@ +resources: + - ../../base + - configmap.yaml +{{- if not .Restricted }} + - secret.yaml +{{- end }} diff --git a/templates/deployment/kustomize/overlays/environment/secret.yaml.tmpl b/templates/deployment/kustomize/overlays/environment/secret.yaml.tmpl new file mode 100644 index 0000000..65a0c19 --- /dev/null +++ b/templates/deployment/kustomize/overlays/environment/secret.yaml.tmpl @@ -0,0 +1,11 @@ +{{- if not .Restricted }} +apiVersion: v1 +kind: Secret +metadata: + name: secret-{{ .Service.Name.DNSCase }} + namespace: "{{ .Namespace }}" +data: +{{- range $key, $value := .SecretMap }} + {{ $key }}: {{ printf "%q" $value }} +{{- end }} +{{- end }} diff --git a/templates/factory/.gitignore.tmpl b/templates/factory/.gitignore.tmpl new file mode 100644 index 0000000..4460ccc --- /dev/null +++ b/templates/factory/.gitignore.tmpl @@ -0,0 +1,14 @@ +# The compiled backend +app +{{ .Service.Name.LowerCase }} + +# Go workspace (keep the module-level go.mod pinned) +go.work +go.work.sum + +# Test binaries and coverage +*.test +*.out + +# Codefly local artifacts +.codefly/ diff --git a/templates/factory/GETTING_STARTED.md.tmpl b/templates/factory/GETTING_STARTED.md.tmpl new file mode 100644 index 0000000..d83254e --- /dev/null +++ b/templates/factory/GETTING_STARTED.md.tmpl @@ -0,0 +1,69 @@ +# The {{ .Module.Name.LowerCase }} solution's backend + +Scaffolded by `codefly.dev/solution-go`. Two directories were written: + +- `services/{{ .Service.Name.LowerCase }}` — this service, the solution's entry service. +- `services/fe-remote` — the `./Page` Module Federation remote. An npm package, + not a Codefly service, which is why nothing else writes it. + +## What is left, in order + +1. `cd services/{{ .Service.Name.LowerCase }} && go build ./... && go test ./...` +2. `cd services/fe-remote && npm install && npm test && npm run typecheck` + The `@codefly-dev/*` kits come from GitHub Packages, so `NODE_AUTH_TOKEN` + must be set to a token with `read:packages` (see `.npmrc`). +3. Declare what the page may read, under `api.consumes` in the solution's + `solution.codefly.yaml`, then `codefly sync` to regenerate `consumed_gen.go` + and fill in that module's entry in `consumes.go`. + Add a binding to `module.contract.codefly.yaml` for each one, and have the + composition fill its two slots — `_ENDPOINT` and `_BINDING` in this + solution's own configuration group. The runtime opens a relay for every + declared module at boot and refuses the boot while a slot is empty. +4. Serve a route of your own: add a `solution.Operation` in `operations.go`, + then `go generate ./...` to re-render `openapi/api.swagger.json`. That + document is what `codefly generate contracts` publishes as this endpoint's + contract, and the CLI derives the rows the authority's Check applies from it. +5. Read a configuration value with `github.com/codefly-dev/sdk-go` + (`codefly.For(ctx).WorkspaceConfiguration(group, key)`) and add it to + `go.mod` when you do. Nothing is scaffolded that reads one. + +## If this service was added with `codefly add service` rather than `codefly add solution` + +`codefly add solution` writes the five declarations a solution is installed +from and the shape of this service's manifest. Added on its own, this service +has none of them, and it needs all of this in `service.codefly.yaml`: + +```yaml +module-identity: true +service-dependencies: + - interface: codefly.dev/authority@>=0.2.0 <0.4.0 + - name: hop + module: authority + kind: external + endpoints: + - name: grpc +workspace-configuration-dependencies: + - authority + - {{ .Module.Name.LowerCase }} +endpoints: + - name: http + visibility: public + exposure: none + api: http + secured: false + health: + readiness: + kind: http + path: /health + statuses: ["200"] +``` + +Never add a `service-dependencies` entry for a module this solution consumes. +Every module is reached through `authority/hop`, which checks the call; a +direct edge renders into a mesh allow rule past that check. + +It also needs `solution.frontend.codefly.yaml` beside this file, which +`main.go` embeds, and the solution's own `solution.codefly.yaml`, +`module.contract.codefly.yaml` and `module.package.codefly.yaml` one directory +above `services/`. `codefly add solution --package-id / +--agent solution-go` writes all of them. diff --git a/templates/factory/consumes.go.tmpl b/templates/factory/consumes.go.tmpl new file mode 100644 index 0000000..4a6122b --- /dev/null +++ b/templates/factory/consumes.go.tmpl @@ -0,0 +1,98 @@ +package main + +import ( + "strings" + + solution "github.com/codefly-dev/solution-runtime-go" +) + +// consumed is every module call this solution's page may make through this +// backend, and nothing else of any module is reachable from it. Each call is +// relayed by the runtime's passthrough to the authority's hop in front of the +// module, AS THE VIEWER — a child of the person's Work Context, issued through +// this solution's binding for that module (module.contract.codefly.yaml) — and +// the module authorizes it. This declaration only narrows what may be asked +// and what comes back. +// +// THE MODULES ARE NOT NAMED HERE. They come from consumedAsAll, which is +// generated from api.consumes in the solution's solution.codefly.yaml: that +// manifest is the one place this solution says what it may reach, and a second +// list here is a second declaration that drifts. What this file adds is the +// part no manifest can carry — the authority each call asks for, and the +// module methods it may call, which are constants of that module's generated +// Go client. +// +// A module with no entry in `composition` below is declared with no method, so +// nothing of it is reachable. That is the scaffolded state. +func consumed() []solution.ConsumedModule { + modules := make([]solution.ConsumedModule, 0, len(consumedAsAll)) + for _, as := range consumedAsAll { + declared := composition[as] + modules = append(modules, solution.ConsumedModule{ + As: as, + Scopes: declared.Scopes, + Methods: declared.Methods, + Authority: relayFor(as), + }) + } + return modules +} + +// consumedComposition is what this solution asks for at one module: the +// authority it asks the authority to issue for the viewer, and the methods it +// may call. A method that declares its own Scopes asks only for those. +type consumedComposition struct { + Scopes []solution.Scope + Methods []solution.ConsumedMethod +} + +// composition is that, per module, keyed by the `as` api.consumes gives it. +// Add the module's Go client to go.mod and name its procedures from the +// generated package, never as string literals — a renamed procedure must fail +// to compile here rather than 404 at the hop: +// +// var composition = map[string]consumedComposition{ +// consumedAs.Documents: { +// Scopes: []solution.Scope{ +// {ResourceKind: "documents", Actions: []string{"read"}}, +// {ResourceKind: "documents.collection", Actions: []string{"read"}}, +// }, +// Methods: []solution.ConsumedMethod{ +// {Name: documentsv1connect.DocumentsServiceListCollectionProcedure, +// Response: solution.MustFieldMask(&documentsv1.ListCollectionResponse{}, "documents.entry.entry_id", "documents.entry.path")}, +// }, +// }, +// } +// +// Response is an ALLOWLIST, named leaf by leaf, so a field the module adds +// later is dropped until someone names it. Omitting it sends the whole +// message to the page. +var composition = map[string]consumedComposition{} + +// slotsGroup is this solution's own workspace configuration group: where the +// composition says, per consumed module, where it is and which of this +// solution's bindings the viewer's calls are issued through. +const slotsGroup = solutionID + +// relayFor is those two slots for one consumed module. +// +// They are the COMPOSITION's to fill, never this repository's: `_ENDPOINT` +// holds the grpc address of the hop in front of the module (an +// ${endpoint:authority/hop/grpc} reference the run resolves) and `_BINDING` +// the id the composition installs this solution's binding for that module +// under. The runtime opens a relay for every declared module at boot and +// refuses the boot, by name, while either is empty — which is the right +// moment to find out. +func relayFor(as string) solution.AuthorityRelay { + return solution.AuthorityRelay{ + Group: slotsGroup, + EndpointKey: slotKey(as, "ENDPOINT"), + BindingKey: slotKey(as, "BINDING"), + } +} + +// slotKey is the configuration key an `as` yields: "document-store" and +// "ENDPOINT" give DOCUMENT_STORE_ENDPOINT. +func slotKey(as, suffix string) string { + return strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(as)) + "_" + suffix +} diff --git a/templates/factory/consumes_test.go.tmpl b/templates/factory/consumes_test.go.tmpl new file mode 100644 index 0000000..9bddc99 --- /dev/null +++ b/templates/factory/consumes_test.go.tmpl @@ -0,0 +1,129 @@ +package main + +import ( + "os" + "path/filepath" + "slices" + "testing" + + "gopkg.in/yaml.v3" +) + +// solutionManifestPath is the solution's own manifest: api.consumes lives +// there, one directory above services/, where `codefly add solution` writes it +// and where the platform's gates read it. +const solutionManifestPath = "../../solution.codefly.yaml" + +// serviceManifestPath is this service's manifest, beside this file. +const serviceManifestPath = "service.codefly.yaml" + +// TestConsumedMatchesTheManifest is the static half of a check that otherwise +// only shows up at a boot nobody runs until the page is broken. +// +// consumed_gen.go is generated from api.consumes by `codefly sync`, and +// consumed() is derived from it, so the two cannot name different modules. +// What CAN go stale is the generated file itself: a module added to +// api.consumes reaches nothing until `codefly sync` runs. This fails until it +// has. +func TestConsumedMatchesTheManifest(t *testing.T) { + declared := []string{} + for _, module := range consumed() { + declared = append(declared, module.As) + } + slices.Sort(declared) + + wanted := append([]string{}, manifestAliases(t)...) + slices.Sort(wanted) + + if !slices.Equal(declared, wanted) { + t.Fatalf("this backend consumes %v; %s declares %v.\n"+ + "consumed_gen.go is generated from api.consumes: run `codefly sync` and commit it.", + declared, solutionManifestPath, wanted) + } +} + +// TestEveryConsumedModuleIsReachedThroughTheHop pins that every module call +// goes to the authority's hop at an address and a binding THE COMPOSITION +// supplies, never to an address this repository names. A declaration missing +// either slot is a boot that fails naming it, so this is the same refusal one +// step earlier. +func TestEveryConsumedModuleIsReachedThroughTheHop(t *testing.T) { + for _, module := range consumed() { + relay := module.Authority + if relay.Group == "" || relay.EndpointKey == "" || relay.BindingKey == "" { + t.Errorf("%q names no hop slots (group=%q endpoint=%q binding=%q): the composition says where a module is and which binding reaches it", + module.As, relay.Group, relay.EndpointKey, relay.BindingKey) + } + } +} + +// TestNoModuleEdgeIsDeclared is SP-SOL-04 in this solution's own words: a +// solution reaches every module through the authority's hop, which checks the +// call, and a `service-dependencies` edge on a module renders into a mesh +// allow rule straight to that module's service, PAST the check. +// +// The only edges this service may hold are the host's own door: the authority +// interface it requires by `interface:` alone, and `authority/hop`. The +// consumer repository's check-solution-mesh gate refuses anything else; this +// fails here first, in `go test`, where whoever added the edge is standing. +func TestNoModuleEdgeIsDeclared(t *testing.T) { + var document struct { + Dependencies []struct { + Interface string `yaml:"interface"` + Name string `yaml:"name"` + Module string `yaml:"module"` + } `yaml:"service-dependencies"` + } + content, err := os.ReadFile(serviceManifestPath) + if err != nil { + t.Fatalf("cannot read %s: %v", serviceManifestPath, err) + } + if err := yaml.Unmarshal(content, &document); err != nil { + t.Fatalf("cannot read %s: %v", serviceManifestPath, err) + } + for _, dependency := range document.Dependencies { + switch { + case dependency.Module == "": + // An interface requirement, or a dependency on this solution's own + // service. Neither names a module service. + case dependency.Module == "authority" && dependency.Name == "hop": + // The authority's door: it lets through only a call the authority + // checked. + default: + t.Errorf("%s declares a direct path to %s/%s. A solution reaches a module through the authority's hop, which checks the call; this edge renders into a mesh allow rule past it (SP-SOL-04). Declare the module under api.consumes instead.", + serviceManifestPath, dependency.Module, dependency.Name) + } + } +} + +// manifestAliases reads the `as` of every api.consumes entry. A missing +// manifest fails rather than reading as "consumes nothing": this service is a +// solution's entry service, and a solution without a manifest is not installed +// by anything. +func manifestAliases(t *testing.T) []string { + t.Helper() + content, err := os.ReadFile(filepath.FromSlash(solutionManifestPath)) + if err != nil { + t.Fatalf("cannot read %s, which every reader of this solution reads: %v", solutionManifestPath, err) + } + var document struct { + API struct { + Consumes []struct { + ID string `yaml:"id"` + As string `yaml:"as"` + } `yaml:"consumes"` + } `yaml:"api"` + } + if err := yaml.Unmarshal(content, &document); err != nil { + t.Fatalf("cannot read %s: %v", solutionManifestPath, err) + } + aliases := []string{} + for _, consumed := range document.API.Consumes { + alias := consumed.As + if alias == "" { + alias = consumed.ID + } + aliases = append(aliases, alias) + } + return aliases +} diff --git a/templates/factory/go.mod.tmpl b/templates/factory/go.mod.tmpl new file mode 100644 index 0000000..58cbbf5 --- /dev/null +++ b/templates/factory/go.mod.tmpl @@ -0,0 +1,80 @@ +module {{ .SolutionID }}/backend + +go 1.27.3 + +// Pinned by the solution-go agent, so a new solution does not have to work the +// graph out. The runtime pin is a RELEASE, never a pseudo-version: a +// pseudo-version names a commit that may never reach a tag, and +// obin-ai/core-solutions' check-consumed-apis gate requires the native +// projected-token runtime (v0.0.12 or later). +// +// This block is `go mod tidy`-clean as generated, with go.sum beside it, so a +// freshly scaffolded backend builds with the default -mod=readonly and no +// network. Moving any pin is a commit that re-runs the solution's boot, not a +// tidy. + +require ( + github.com/codefly-dev/solution-runtime-go v0.0.16 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + connectrpc.com/connect v1.21.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec // indirect + github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec // indirect + github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af // indirect + github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 // indirect + github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 // indirect + github.com/go-openapi/analysis v1.0.0 // indirect + github.com/go-openapi/errors v0.22.9 // indirect + github.com/go-openapi/jsonpointer v1.0.1 // indirect + github.com/go-openapi/jsonreference v1.0.2 // indirect + github.com/go-openapi/loads v0.25.3 // indirect + github.com/go-openapi/spec v1.0.1 // indirect + github.com/go-openapi/strfmt v0.27.2 // indirect + github.com/go-openapi/swag/conv v0.29.2 // indirect + github.com/go-openapi/swag/jsonutils v0.29.2 // indirect + github.com/go-openapi/swag/loading v0.29.2 // indirect + github.com/go-openapi/swag/mangling v0.29.2 // indirect + github.com/go-openapi/swag/pools v0.29.2 // indirect + github.com/go-openapi/swag/stringutils v0.29.2 // indirect + github.com/go-openapi/swag/typeutils v0.29.2 // indirect + github.com/go-openapi/swag/yamlutils v0.29.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gofrs/flock v0.13.1 // indirect + github.com/google/go-github/v89 v89.0.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modelcontextprotocol/go-sdk v1.8.0 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/templates/factory/go.sum.tmpl b/templates/factory/go.sum.tmpl new file mode 100644 index 0000000..fda2a72 --- /dev/null +++ b/templates/factory/go.sum.tmpl @@ -0,0 +1,271 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs= +connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +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/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= +github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec h1:gjLHn5hsteMDx8TeNj+KPBsa06nLwzAEhyR2IfC3JWs= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= +github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec h1:QhncdaWaMERAGpZthlSXjKroXNl/L8llPtBMX+e5w/A= +github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:NqCbvIWbkbCzSpUUYQjP8Jv+LpFnDRbVhPdAj67DHhE= +github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af h1:JLTecMs4Oja+fl51chLyGw262S4yz7jzEEiFxoq7Viw= +github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= +github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= +github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 h1:hi+KhotTLMOJ/PUSzVO/Axfho7yZ2UOjrCFfrLI1EKU= +github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458/go.mod h1:Qg8SMzZxKSllwvZwShMrWgSetBVMjxP0s17mwWHWjk0= +github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 h1:wBOL7QsuoC0HsS43gsH2+tpsipk4os077C2VUDkMUHg= +github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458/go.mod h1:hwtrdGtgh8i2xzjgihyd4wY2u+UK7nHQz30OGKL0n+M= +github.com/codefly-dev/solution-runtime-go v0.0.16 h1:I6BpEvzw3PsMYopyDfBCVx2Zcf7yFh6uNqCQcz6MxR0= +github.com/codefly-dev/solution-runtime-go v0.0.16/go.mod h1:sfriXiK34I7k5Ad1Y1VxxNdChsSmzmEJEj3s+IwQlfk= +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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= +github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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/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.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v1.0.0 h1:sNvbAGCJqUTqIAodr9IVqJMmuZas3YS9ms1dGK9yiJ4= +github.com/go-openapi/analysis v1.0.0/go.mod h1:NhYjJ57fnE+bcE7UwrJyMkhWA3Dfz7TdiBfTVAnos4Y= +github.com/go-openapi/errors v0.22.9 h1:HI9+SyVYiRzyeBQGv0CbWbQa+u2S0nny7G7AhY+UMiM= +github.com/go-openapi/errors v0.22.9/go.mod h1:R73q66sXP2smzAGgFOI4nqtusRK2xN5pYCjsij+KX58= +github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg= +github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY= +github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc= +github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs= +github.com/go-openapi/loads v0.25.3 h1:V+jKy/thXWdLJUuYC8sZX2dICyAa8M3DokF70jj34P0= +github.com/go-openapi/loads v0.25.3/go.mod h1:LgLyCSOLBL2Qnj0Ps1oo2YH8jZoEKsMeVZCc+ALIxFo= +github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk= +github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8= +github.com/go-openapi/strfmt v0.27.2 h1:SG32SlbwNy92s0KJiVxt2joJeFdqIYHvwrA0OU6HqzQ= +github.com/go-openapi/strfmt v0.27.2/go.mod h1:M4CKsMO0Fb8qR10+1Ra75wCKNNquy+Vj+4LWZrhTo2E= +github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= +github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= +github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= +github.com/go-openapi/swag/cmdutils v0.29.2/go.mod h1:XziaSVzYqkDvpWXzASfWajw2YvJrLscq610O2XiyqJw= +github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0= +github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q= +github.com/go-openapi/swag/fileutils v0.29.2 h1:mdUL+Vw5ah1fO1AvFCoHeIyv7YZsCz2MN3KNqd/lLVI= +github.com/go-openapi/swag/fileutils v0.29.2/go.mod h1:7QKmmodjAebaq61lGVJa8ldLdGeMF4DSgYv8tXOGGo0= +github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k= +github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM= +github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY= +github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI= +github.com/go-openapi/swag/mangling v0.29.2 h1:ltdo3K0tTP4P6zWhVVWDU9D37/YYBuMJRdZxiS2Dtog= +github.com/go-openapi/swag/mangling v0.29.2/go.mod h1:RjDi4TnItAenS3cgCXSpIl1kKbjG7il6pnmQVl/8bp8= +github.com/go-openapi/swag/netutils v0.29.2 h1:dBli6jyUa93sDS/XYWShJzf4+nnVfFKfvY9SLUaUzB8= +github.com/go-openapi/swag/netutils v0.29.2/go.mod h1:1pM6Xg/Um8E1eps3umWl8Hme3ByWXOtHsvFC0CULHUE= +github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c= +github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc= +github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk= +github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI= +github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I= +github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE= +github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM= +github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= +github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho= +github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo= +github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE= +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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +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-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0= +github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho= +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/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +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/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-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/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/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +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/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= +github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +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/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4= +github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74= +github.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +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/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 h1:Tq+E65HaNbZTeqcLw6QUhsV+/EIWdkfUH2CQzcrlE5M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0/go.mod h1:A+EVhDakAj1waJBPhmTszZaSr08ww8MRRKy1nB1rqck= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +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.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +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.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +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.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b h1:nB3spRIaSPtxJpxb0k41yljROvm2Jrwfs2pSNdKS5y8= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b/go.mod h1:9N4vtjEwvgQKEdlFVQO0M44ErhecQ9DCSQfGoZ51M6w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 h1:QG6hS4zFplRZvOQedzei4t2pT/zkQc3VR1u+S9/rcAw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/templates/factory/main.go.tmpl b/templates/factory/main.go.tmpl new file mode 100644 index 0000000..754db35 --- /dev/null +++ b/templates/factory/main.go.tmpl @@ -0,0 +1,63 @@ +// Command backend is the server for the "{{ .Title }}" solution. +// +// Everything a solution needs identically is the solution runtime's: it +// presents what this solution is, answers the health probe the endpoint +// declares, serves the page's calls, and relays the page's module calls to +// the authority's hop AS THE VIEWER, where each module authorizes the call. +// +// What is left here is this solution's own composition: what it is (below), +// which routes it serves itself (operations.go), and which module operations +// its page may call (consumes.go). +package main + +import ( + _ "embed" + "log" + + solution "github.com/codefly-dev/solution-runtime-go" +) + +// The runtime and the CLI read ONE declared frontend block: the runtime serves +// it at /.well-known/solution.json and the CLI delivers it in the presence +// document it signs. Keeping it beside this service — and embedding it rather +// than restating it — is what keeps the served manifest and the signed +// presence from becoming two files that disagree. +// +//go:embed solution.frontend.codefly.yaml +var frontendDeclaration []byte + +// solutionID is this solution's ONE name: the id the host's presence document +// names it by, the alias the authority's edge routes it at (/v1/{{ .SolutionID }}), +// the principal the authority issues to, and the Codefly module name. +const solutionID = "{{ .SolutionID }}" + +// manifest is what this solution is. +// +// Capabilities are the feature ids the host's capability handshake reports +// (/.well-known/capabilities). Name one per thing this solution can actually +// do — "documents.read" once its page reads the document store, "read-only" +// when it writes nothing anywhere — and nothing it cannot: a host lists a +// solution by what it claims here. +var manifest = solution.Manifest{ + ID: solutionID, + Contract: solutionID, + Frontend: frontendDeclaration, +} + +func main() { + // A composition that names no authority is refused at boot, by name: + // there is no other way from here to a module. + if err := server().Serve(); err != nil { + log.Fatal(err) + } +} + +// server is this solution on the authority interface: the routes it serves +// itself, and the modules it consumes relayed by the runtime's passthrough. +// +// ONE declaration both serves the solution and renders its interface document +// (openapi.go), so a route cannot be served without publishing the policy a +// call to it must hold, nor published without being served. +func server() *solution.Server { + return solution.New(manifest).Consumes(consumed()...).Operations(operations()...) +} diff --git a/templates/factory/openapi.go.tmpl b/templates/factory/openapi.go.tmpl new file mode 100644 index 0000000..8e33e10 --- /dev/null +++ b/templates/factory/openapi.go.tmpl @@ -0,0 +1,28 @@ +package main + +import solution "github.com/codefly-dev/solution-runtime-go" + +//go:generate go test -run TestInterfaceArtifactIsCurrent -update . + +// interfaceArtifactPath is this backend's interface document: every route it +// serves under the authority interface, each with the policy a call to it must +// hold, rendered by the solution runtime from the server itself at the version +// service.codefly.yaml declares. +// +// It is the document `codefly generate contracts` publishes as the http +// endpoint's contract, and the CLI derives the route rows the authority's +// Check applies from it — so a backend without one publishes no contract, and +// a route served without being in it is a route the authority has no row for. +// `go generate ./...` rewrites it and openapi_test.go fails on a diff. +const interfaceArtifactPath = "openapi/api.swagger.json" + +// renderInterfaceArtifact renders the document for the backend rooted at dir. +func renderInterfaceArtifact(dir string) ([]byte, error) { + return server().InterfaceArtifact(dir, solution.InterfaceInfo{ + Title: "{{ .SolutionID }} solution backend", + Description: "{{ .InterfaceDescription }}", + // No base path: every route already carries /v1/{{ .SolutionID }}, as served. + OperationPrefix: "{{ .OperationPrefix }}", + Tag: solutionID, + }) +} diff --git a/templates/factory/openapi_test.go.tmpl b/templates/factory/openapi_test.go.tmpl new file mode 100644 index 0000000..6213af2 --- /dev/null +++ b/templates/factory/openapi_test.go.tmpl @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "flag" + "os" + "path/filepath" + "testing" +) + +var update = flag.Bool("update", false, "rewrite "+interfaceArtifactPath+" from the routes this backend serves") + +// TestInterfaceArtifactIsCurrent holds the committed interface document to the +// routes this backend actually serves. It is rendered from the server itself, +// so a route added without re-rendering fails here rather than reaching the +// authority with no row to check it against. +// +// `go generate ./...` runs this with -update. +func TestInterfaceArtifactIsCurrent(t *testing.T) { + rendered, err := renderInterfaceArtifact(".") + if err != nil { + t.Fatalf("cannot render %s: %v", interfaceArtifactPath, err) + } + path := filepath.FromSlash(interfaceArtifactPath) + if *update { + if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, rendered, 0o600); err != nil { + t.Fatal(err) + } + return + } + committed, err := os.ReadFile(path) + if err != nil { + t.Fatalf("cannot read %s: %v\nRun `go generate ./...` and commit it.", interfaceArtifactPath, err) + } + if !bytes.Equal(bytes.TrimSpace(committed), bytes.TrimSpace(rendered)) { + t.Fatalf("%s is not what this backend serves. Run `go generate ./...` and commit it.", interfaceArtifactPath) + } +} diff --git a/templates/factory/operations.go.tmpl b/templates/factory/operations.go.tmpl new file mode 100644 index 0000000..fe43105 --- /dev/null +++ b/templates/factory/operations.go.tmpl @@ -0,0 +1,40 @@ +package main + +import solution "github.com/codefly-dev/solution-runtime-go" + +// route is where the authority's edge routes this solution and forwards its +// requests unchanged: /v1/, the solution's alias. EVERY operation carries +// it, because the runtime asks the authority about the route as served — a +// path declared without it is a row no request matches. +const route = "/v1/" + solutionID + +// operations is what this backend serves itself, beside the runtime's +// passthrough. +// +// A freshly scaffolded solution serves none of its own: the host lists it, +// mounts its page, and the page reads what it reads through the modules this +// solution consumes (consumes.go). Add one per route this solution answers +// with work that is its own. +// +// Policy is REQUIRED on every operation. The authority applies it to each +// call, and an absent declaration and "nothing in particular is required" +// must not look the same — so a route that needs nothing beyond a checked +// caller says so with solution.AnyCheckedCaller(): +// +// solution.Operation{ +// Path: route + "/notes", Method: http.MethodGet, +// Summary: "list the notes the viewer may read", +// Callers: "The page's Notes view.", +// Behavior: "Answers the viewer's own notes, newest first, 50 at a time.", +// Response: notesResponse{}, +// Policy: solution.Requires("{{ .SolutionID }}.note", "read"), +// Handler: listNotes, +// } +// +// The resource kind a policy names is this solution's OWN vocabulary, which +// module.contract.codefly.yaml declares under scope_ceilings. A kind the +// contract does not declare is a scope the authority never issues, so the +// route would refuse every call. +func operations() []solution.Operation { + return nil +} diff --git a/templates/page/.npmrc.tmpl b/templates/page/.npmrc.tmpl new file mode 100644 index 0000000..e03bcd4 --- /dev/null +++ b/templates/page/.npmrc.tmpl @@ -0,0 +1,5 @@ +# The @codefly-dev kits are published to GitHub Packages, so this remote's +# install needs a token with `read:packages` in NODE_AUTH_TOKEN. Ordinary peer +# resolution is required; no peer bypass. +@codefly-dev:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} diff --git a/templates/page/README.md.tmpl b/templates/page/README.md.tmpl new file mode 100644 index 0000000..5253a4c --- /dev/null +++ b/templates/page/README.md.tmpl @@ -0,0 +1,23 @@ +# {{ .Title }} — the `./Page` remote + +The Module Federation remote a saas host federates, and the page the solution's +own shell (`../web`) serves where no host shell mounts it. Both read one build. + +It is an **npm package, not a Codefly service**: no `codefly add service` call +reaches it, which is why the `solution-go` agent writes it from the backend's +own Create. + +```sh +npm install # NODE_AUTH_TOKEN needs read:packages — see .npmrc +npm test +npm run typecheck +npm run build # the backend serves the output at /assets/ +``` + +- `src/binding.ts` is the host↔remote contract, picked from + `@codefly-dev/saas-ui/solution`'s `SolutionBinding`. Never restate a field + there and never require one an older host may not inject. +- `rsbuild.config.ts` carries the shared-singleton policy. Every entry in it + must be the host's single instance; read the comments before changing one. +- The title and nav order the host's shell shows come from the backend's + `solution.frontend.codefly.yaml`, not from this package. diff --git a/templates/page/package.json.tmpl b/templates/page/package.json.tmpl new file mode 100644 index 0000000..2069be6 --- /dev/null +++ b/templates/page/package.json.tmpl @@ -0,0 +1,38 @@ +{ + "name": "{{ .SolutionID }}-fe-remote", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + "./Page": "./src/Page.tsx" + }, + "scripts": { + "dev": "rsbuild dev", + "build": "rsbuild build", + "preview": "rsbuild preview", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0", + "@codefly-dev/saas-plugin-contract": "^2.4.2", + "@codefly-dev/saas-plugin-react": "^0.4.3", + "@codefly-dev/saas-sdk": "^0.3.16", + "@codefly-dev/saas-ui": "^0.12.5", + "@codefly-dev/ui": "^0.12.5" + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "2.9.0", + "@rsbuild/core": "^2.2.8", + "@rsbuild/plugin-react": "^2.1.0", + "@testing-library/dom": "^10.4.2", + "@testing-library/react": "^16.1.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "jsdom": "^30.1.0", + "typescript": "^7.0.2", + "vite": "^8.3.0", + "vitest": "^5.0.1" + } +} diff --git a/templates/page/rsbuild.config.ts.tmpl b/templates/page/rsbuild.config.ts.tmpl new file mode 100644 index 0000000..9af0036 --- /dev/null +++ b/templates/page/rsbuild.config.ts.tmpl @@ -0,0 +1,68 @@ +import { pluginModuleFederation } from "@module-federation/rsbuild-plugin"; +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; + +// The backend serves this static remote at /assets/, and a saas host federates +// the same `./Page` directly. Both read one build. +// +// `name` is emitted by Module Federation as a JavaScript GLOBAL, so it must be +// an identifier: "{{ .SolutionID }}" yields "{{ .Identifier }}". The +// solution-go agent refuses a module name that cannot become one, so a bundler +// never has to. +const name = "{{ .Identifier }}"; + +// EVERY ENTRY HERE MUST BE THE HOST'S SINGLE INSTANCE, or its React context +// and its skin tokens do not line up across the federation boundary and the +// kit renders unstyled, stale, or not at all. These are not performance +// settings. +const hostSingleton = { singleton: true, requiredVersion: false } as const; + +// `import: false` means "never bundle a copy": the host always provides these, +// and a second copy of a plugin entrypoint is a second React context. +const hostProvided = { ...hostSingleton, import: false } as const; + +const shared = { + react: { ...hostSingleton }, + "react/jsx-runtime": { ...hostSingleton }, + "react-dom": { ...hostSingleton }, + // A share key is an EXACT specifier. A kit that mounts its own root with + // createRoot from react-dom/client gets its own react-dom build unless this + // exact key is shared, and that build refuses the host's React (React error + // #527) — the whole page then fails to load. + "react-dom/client": { ...hostSingleton }, + "@codefly-dev/ui": { ...hostSingleton }, + "@codefly-dev/saas-ui": { ...hostSingleton }, + "@codefly-dev/saas-sdk": { ...hostSingleton }, + // Each plugin entrypoint must have one identity in a direct import and in + // the UI wrappers alike. + "@codefly-dev/saas-plugin-contract": { ...hostProvided }, + "@codefly-dev/saas-plugin-contract/capabilities": { ...hostProvided }, + "@codefly-dev/saas-plugin-react": { ...hostProvided }, + "@codefly-dev/saas-plugin-react/runtime": { ...hostProvided }, + "@codefly-dev/saas-plugin-react/ui": { ...hostProvided }, +}; + +// "auto" makes the container resolve its chunk URLs at runtime, relative to +// wherever its remoteEntry was loaded from, so one build is servable from any +// backend origin with none baked in. +const assetPrefix = process.env.ASSET_PREFIX ?? "auto"; + +// PORT is the dev server's; nothing deployed reads it, and the backend's own +// port is Codefly-assigned. +const port = Number(process.env.PORT ?? 3100); + +export default defineConfig({ + plugins: [pluginReact(), pluginModuleFederation({ name, exposes: { "./Page": "./src/Page.tsx" }, dts: false, shared })], + // The host is a different origin and fetches the remote entry and its chunks. + server: { port, cors: true }, + dev: { assetPrefix }, + output: { + assetPrefix, + // Inline this remote's CSS into its JS. A host's CSP allows + // `style-src 'self' 'unsafe-inline'` and does not widen for a solution + // origin, so a cross-origin is refused and the page + // fails to mount ("Loading CSS chunk … failed"). Injected styles are + // covered by 'unsafe-inline'. + injectStyles: true, + }, +}); diff --git a/templates/page/src/Page.test.tsx.tmpl b/templates/page/src/Page.test.tsx.tmpl new file mode 100644 index 0000000..b48d423 --- /dev/null +++ b/templates/page/src/Page.test.tsx.tmpl @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import Page from "./Page"; + +// vitest runs without globals here, so testing-library's automatic cleanup is +// not installed: without this each test renders into the previous test's DOM +// and a query that should find one node finds two. +afterEach(cleanup); + +// A host mounts `./Page` with its own props and nothing else. These are the +// cases that blank the page in a browser rather than failing a build. +describe("the page a host mounts", () => { + const props = { solutionId: "{{ .SolutionID }}", apiBase: "/api/solutions/{{ .SolutionID }}/proxy" }; + + it("mounts for a signed-out viewer without throwing", () => { + render(); + expect(screen.getByText(/Sign in/)).toBeTruthy(); + }); + + it("carries no installation id in its source", async () => { + // Which installation a page is mounted for is read at runtime, from a + // route this solution's backend serves — never pasted in. A UUID + // anywhere in this remote's source is that rule broken, and this is the + // only place that notices. + const sources = import.meta.glob("./**/*.{ts,tsx}", { query: "?raw", import: "default", eager: true }) as Record; + expect(Object.keys(sources).length).toBeGreaterThan(1); + const uuid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; + for (const [path, source] of Object.entries(sources)) { + expect(uuid.test(source), `${path} carries a UUID`).toBe(false); + } + }); +}); diff --git a/templates/page/src/Page.tsx.tmpl b/templates/page/src/Page.tsx.tmpl new file mode 100644 index 0000000..468bc3a --- /dev/null +++ b/templates/page/src/Page.tsx.tmpl @@ -0,0 +1,58 @@ +import { useMemo } from "react"; +import { requestBinding, useAccessToken, useViewerEpoch, viewerOrganization } from "@codefly-dev/saas-ui/solution"; +import type { PageProps } from "./binding"; +import "./page.css"; + +/** + * {{ .Title }} — the page a host mounts as `./Page`. + * + * The title and the nav order the host's shell shows are NOT here: they are + * declared once, in the backend's `solution.frontend.codefly.yaml`, which the + * backend embeds and the CLI delivers in the presence document it signs. What + * this file is, is the page itself. + * + * Every call this page makes goes to `apiBase` — one same-origin base, because + * the host's proxy routes on the path — and arrives at the backend carrying + * the viewer's own Work Context, where the authority checks it against the + * route's published policy. + */ +export default function Page(props: PageProps) { + // A new viewer gets a new page rather than the previous viewer's state. + const epoch = useViewerEpoch(props.getAccessToken, props.subscribeToken); + return ; +} + +function SolutionPage({ apiBase, getAccessToken, authedFetch, subscribeToken }: PageProps) { + const token = useAccessToken(getAccessToken, subscribeToken); + const organizationId = viewerOrganization(token) ?? ""; + // A signed-out viewer has no credential, and so no binding to reach + // anything with. Build none rather than letting requests go out + // anonymously and come back as an ordinary sign-in 401. + const binding = useMemo( + () => (getAccessToken || authedFetch ? requestBinding(apiBase, getAccessToken, authedFetch) : null), + [apiBase, getAccessToken, authedFetch], + ); + + if (!binding || !organizationId) { + return ( +
+

Sign in and select an organization to use {{ .Title }}.

+
+ ); + } + + return ( +
+
+

{{ .Title }}

+

This page is yours to write. It reaches its backend through {apiBase}.

+
+
+

+ Add a view here. Call a route this solution serves (its backend's operations.go), or a + module operation it consumes, at {`${apiBase}/modules///`}. +

+
+
+ ); +} diff --git a/templates/page/src/binding.ts.tmpl b/templates/page/src/binding.ts.tmpl new file mode 100644 index 0000000..8e5b611 --- /dev/null +++ b/templates/page/src/binding.ts.tmpl @@ -0,0 +1,24 @@ +import type { SolutionBinding } from "@codefly-dev/saas-ui/solution"; + +/** + * THE HOST↔REMOTE CONTRACT, in this solution's own words. + * + * A host mounts `./Page` with its `SolutionPageProps`, which extends + * `SolutionBinding` — the ONE definition both sides read, in + * `@codefly-dev/saas-ui/solution`. Pick from it; never restate a field, and + * never REQUIRE one a host might not inject: an older host omits it, the + * remote throws inside the host's error boundary, and the page goes blank with + * nothing in the console to say why. + * + * What a host hands over: `solutionId`, `apiBase` (ONE same-origin base for + * every backend call — this solution's own routes and the host's platform + * services alike, because the proxy routes on the path), `getAccessToken`, + * `refreshAccessToken`, `authedFetch`, and optionally `subscribeToken` and + * `declaredSources`. + * + * What it does NOT hand over: which installation of this solution the viewer + * is looking at. A page that needs one asks its own backend for it, on a route + * this solution serves (operations.go); it is never a constant in this source. + */ +export type PageProps = Pick & + Partial>; diff --git a/templates/page/src/page.css.tmpl b/templates/page/src/page.css.tmpl new file mode 100644 index 0000000..8a100bd --- /dev/null +++ b/templates/page/src/page.css.tmpl @@ -0,0 +1,30 @@ +/* + * This remote's own styles, inlined into its JS by rsbuild (injectStyles) — + * see rsbuild.config.ts for why a cross-origin stylesheet cannot load at all. + * + * Keep this file small. The host is the skin authority: colours, type and + * spacing come from @codefly-dev/ui's tokens, which the host provides as a + * shared singleton, so a value hardcoded here is one that stops matching the + * product the first time a skin changes. + */ +.sol-page { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; +} + +.sol-header h1 { + margin: 0; +} + +.sol-main { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.sol-note { + margin: 0; + opacity: 0.8; +} diff --git a/templates/page/tsconfig.json.tmpl b/templates/page/tsconfig.json.tmpl new file mode 100644 index 0000000..bcc583d --- /dev/null +++ b/templates/page/tsconfig.json.tmpl @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["@rsbuild/core/types"] + }, + "include": ["src", "rsbuild.config.ts"] +} diff --git a/templates/page/vitest.config.ts.tmpl b/templates/page/vitest.config.ts.tmpl new file mode 100644 index 0000000..46501c6 --- /dev/null +++ b/templates/page/vitest.config.ts.tmpl @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + include: ["src/**/*.test.{ts,tsx}"], + // @codefly-dev/saas-sdk's published dist imports its generated modules + // without file extensions, which a bundler resolves and Node's ESM loader + // does not. Letting Vite transform it — and saas-ui, which imports it — is + // how a test loads it at all. + server: { deps: { inline: ["@codefly-dev/saas-sdk", "@codefly-dev/saas-ui"] } }, + }, +}); From 638969c35c620cb13f6dce85e6ea61620431d848 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:19:20 -0400 Subject: [PATCH 2/6] fix: hold the shape tests to the rule they state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four shape tests that were failing each caught a real mismatch between what the rule says and what the agent or the test did. Two were the templates' fault: - the ./Page share policy spelled `react` as a bare key while every sibling was a quoted specifier, so the entry that seals the host's React instance read as absent to anything scanning for one. A share key is an exact module specifier; it is now spelled as one. - the scaffolded go.mod named obin-ai/core-solutions in a comment, putting one consumer's private repository into every solution this agent scaffolds for anyone. The pin floor (v0.0.12, the native projected-token runtime) is the fact that matters and is all the comment now states. Two were the tests' own: - the manifest-declaration rule read a fenced YAML example in GETTING_STARTED.md as a declaration. Nothing reads Markdown as a manifest, and that block is deliberately the manifest an author copies when the service was added outside `add solution`. Fenced blocks now come out before matching; prose outside a fence, and every line of a template that is not Markdown, are still held to the rule. - the module-edge rule allowed only GETTING_STARTED to mention service-dependencies, which refused consumes_test.go — the template that READS the manifest in order to refuse an edge, i.e. this very rule enforced inside the scaffolded solution's own suite. Both are now named, with what each says, and an exemption that stops mentioning an edge fails. The edge check itself was replaced rather than relaxed. It matched every `- name:` line in the file, so it read the example's `- name: grpc` — an endpoint of the hop — as a module edge, and would have read a real edge spelled as a flow mapping as nothing at all. The example is now PARSED and held to the host's door structurally, the way check_solution_mesh.py reads a real manifest and for the reason that script gives. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- shape_test.go | 153 +++++++++++++++++++++++--- templates/factory/go.mod.tmpl | 6 +- templates/page/rsbuild.config.ts.tmpl | 6 +- 4 files changed, 146 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index d940687..709763b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/codefly-dev/solution-runtime-go v0.0.16 github.com/stretchr/testify v1.12.1 google.golang.org/grpc v1.84.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -154,7 +155,6 @@ require ( google.golang.org/protobuf v1.36.12 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect diff --git a/shape_test.go b/shape_test.go index a45312d..ee41029 100644 --- a/shape_test.go +++ b/shape_test.go @@ -11,6 +11,7 @@ import ( modulecontract "github.com/codefly-dev/core/contracts/module" "github.com/codefly-dev/core/solution/manifest" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) // THE LINE BETWEEN THIS AGENT AND THE CLI, held by tests rather than by a @@ -78,12 +79,15 @@ func TestNoTemplateIsOneOfTheSolutionsDocuments(t *testing.T) { // probe, and exactly two service-dependencies (the authority interface and its // hop). All of it is the CLI's, so no template may state any of it — not even // as a value that happens to agree today. +// +// A FENCED BLOCK IN A MARKDOWN TEMPLATE IS AN EXAMPLE, not a declaration: +// nothing reads Markdown as a manifest, and GETTING_STARTED.md deliberately +// shows the block an author adds by hand when the service was created outside +// `add solution`. The fence is what marks it as prose, so fenced blocks come +// out before matching and everything else — prose outside a fence, and every +// line of a template that is not Markdown — is still held to the rule. +// TestTheExampleManifestIsTheHostsDoorOnly reads that example structurally. func TestNoTemplateDeclaresWhatTheServiceManifestDeclares(t *testing.T) { - // Matched at the start of a line, so prose and a commented example in - // GETTING_STARTED.md — which deliberately shows the block an author adds - // by hand when the service was created outside `add solution` — is not a - // declaration. A template that is itself a manifest would declare them at - // column zero. declarations := []*regexp.Regexp{ regexp.MustCompile(`(?m)^module-identity:`), regexp.MustCompile(`(?m)^service-dependencies:`), @@ -91,13 +95,35 @@ func TestNoTemplateDeclaresWhatTheServiceManifestDeclares(t *testing.T) { regexp.MustCompile(`(?m)^workspace-configuration-dependencies:`), } for name, content := range templateText(t) { + prose := withoutFencedExamples(name, content) for _, declaration := range declarations { - require.Falsef(t, declaration.MatchString(content), + require.Falsef(t, declaration.MatchString(prose), "%s declares %s, which `codefly add solution` writes after this Create returns", name, declaration) } } } +// withoutFencedExamples is a Markdown template with its fenced code blocks +// removed. A template that is not Markdown is returned whole: a YAML template +// has no fences and no examples, and every line of it is a declaration. +func withoutFencedExamples(name, content string) string { + if !strings.HasSuffix(name, ".md.tmpl") { + return content + } + var prose []string + fenced := false + for _, line := range strings.Split(content, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "```") { + fenced = !fenced + continue + } + if !fenced { + prose = append(prose, line) + } + } + return strings.Join(prose, "\n") +} + // TestNoTemplateNamesAModuleEdge is SP-SOL-04 at the source. A solution // reaches every module through the authority's hop, which CHECKS the call; // Codefly renders a `service-dependencies` edge into a mesh allow rule @@ -107,23 +133,118 @@ func TestNoTemplateDeclaresWhatTheServiceManifestDeclares(t *testing.T) { // The scaffolded consumes_test.go holds a generated solution to the same rule // in its own suite. This holds the agent to never writing one. func TestNoTemplateNamesAModuleEdge(t *testing.T) { - edge := regexp.MustCompile(`(?m)^\s*-\s+name:\s+\S+\s*$`) for name, content := range templateText(t) { if !strings.Contains(content, "service-dependencies") { continue } - // GETTING_STARTED.md is the one template that shows the block, inside - // a fenced example for a service created outside `add solution`. It - // may show the host's own door and nothing else. - require.Equal(t, "templates/factory/GETTING_STARTED.md.tmpl", name, - "%s mentions service-dependencies; only GETTING_STARTED may, as the example an author copies", name) - for _, line := range edge.FindAllString(content, -1) { - require.Containsf(t, line, "hop", - "%s shows a service-dependency on something other than the authority's hop: %q", name, strings.TrimSpace(line)) - } + require.Containsf(t, mayMentionModuleEdges, name, + "%s mentions service-dependencies; only the example an author copies and the scaffolded refusal of an edge may", name) + } + // Both are reached, so neither entry can rot into a stale exemption for a + // template that no longer says anything about an edge. + for name := range mayMentionModuleEdges { + require.Containsf(t, templateText(t)[name], "service-dependencies", + "%s is exempt from the module-edge rule but no longer mentions one: drop the exemption", name) } } +// mayMentionModuleEdges are the two templates that may say +// `service-dependencies`, and what each says. NEITHER DECLARES ONE. +// +// - GETTING_STARTED.md shows the block inside a fenced example, for a +// service created outside `add solution`. +// TestTheExampleManifestIsTheHostsDoorOnly parses it and holds it to the +// host's door. +// - consumes_test.go READS the service manifest in order to refuse an edge: +// it is this very rule, enforced inside the scaffolded solution's own +// suite, so the refusal lands in `go test` where whoever added the edge is +// standing rather than only at the consumer repository's gate. The string +// is a YAML struct tag and the prose explaining it. +var mayMentionModuleEdges = map[string]string{ + "templates/factory/GETTING_STARTED.md.tmpl": "the example an author copies", + "templates/factory/consumes_test.go.tmpl": "the scaffolded refusal of an edge", +} + +// TestTheExampleManifestIsTheHostsDoorOnly reads the YAML example in +// GETTING_STARTED.md STRUCTURALLY, as obin-ai/core-solutions' +// check_solution_mesh.py reads a real manifest and for the same reason: four +// spellings of one object defeat a pattern scan, and all four parse to the +// same value. An earlier line-regex here read the example's `- name: grpc` — +// an endpoint of the hop — as a module edge, and would have read a real edge +// spelled as a flow mapping as nothing at all. +// +// The example is what an author copies when the service was added with +// `codefly add service` rather than `codefly add solution`, so an edge shown +// here becomes an edge in a real manifest. The only ones it may show are the +// host's own door: the authority interface required by `interface:` alone, and +// `authority/hop` as `kind: external`. +func TestTheExampleManifestIsTheHostsDoorOnly(t *testing.T) { + var document struct { + ModuleIdentity bool `yaml:"module-identity"` + Dependencies []struct { + Interface string `yaml:"interface"` + Name string `yaml:"name"` + Module string `yaml:"module"` + Kind string `yaml:"kind"` + } `yaml:"service-dependencies"` + Endpoints []struct { + Name string `yaml:"name"` + Health struct { + Readiness struct { + Path string `yaml:"path"` + } `yaml:"readiness"` + } `yaml:"health"` + } `yaml:"endpoints"` + } + example := fencedYAML(t, templateText(t)["templates/factory/GETTING_STARTED.md.tmpl"]) + require.NoError(t, yaml.Unmarshal([]byte(example), &document), + "the example an author copies must be readable YAML") + + require.True(t, document.ModuleIdentity, + "a solution backend presents the solution's identity: the authority takes its workload credential as the actor of every Work Context it issues") + require.Len(t, document.Dependencies, 2, + "the host's door is the authority interface and its hop, and nothing else") + + // The interface, required by `interface:` alone so the workspace binds it + // to the one authority in scope. check_solution_mesh reads an + // interface-only entry as the door; one that also names a service is an + // ordinary module edge, and the hop is a door ONLY beside this entry. + authority := document.Dependencies[0] + require.Equal(t, "codefly.dev/authority", strings.Split(authority.Interface, "@")[0]) + require.Empty(t, authority.Name, "the interface requirement names no service") + require.Empty(t, authority.Module, "the interface requirement names no module") + + // The authority's hop. `external`: composed by the workspace that composes + // this solution, so it consumes no endpoint and derives no mesh allow rule + // — it names the edge and opens none. + hop := document.Dependencies[1] + require.Equal(t, "authority", hop.Module) + require.Equal(t, "hop", hop.Name) + require.Equal(t, "external", hop.Kind, + "a hop edge that is not external derives a mesh allow rule to the hop's service") + + // The one endpoint, with the readiness probe the scaffolded backend answers. + require.Len(t, document.Endpoints, 1, "a solution backend exposes one endpoint") + require.Equal(t, "http", document.Endpoints[0].Name) + require.Equal(t, "/health", document.Endpoints[0].Health.Readiness.Path) +} + +// templateActions stand where a value goes, so each becomes a scalar before +// the YAML is parsed: `{{ … }}` opens a YAML flow mapping, and that is the +// example's rendering, not its shape. +var templateActions = regexp.MustCompile(`\{\{[^{}]*\}\}`) + +// fencedYAML is the one fenced yaml block in a Markdown template. +func fencedYAML(t *testing.T, content string) string { + t.Helper() + require.NotEmpty(t, content) + _, after, found := strings.Cut(content, "```yaml\n") + require.True(t, found, "the template shows no fenced yaml example") + body, _, closed := strings.Cut(after, "\n```") + require.True(t, closed, "the fenced yaml example is never closed") + return templateActions.ReplaceAllString(body, "example") +} + // TestNoTemplateCarriesProductContent. This agent is published from // codefly-dev and scaffolds a solution for anyone. The template it was modelled // on (obin-ai/core-solutions' templates/solution) depends on that diff --git a/templates/factory/go.mod.tmpl b/templates/factory/go.mod.tmpl index 58cbbf5..790a825 100644 --- a/templates/factory/go.mod.tmpl +++ b/templates/factory/go.mod.tmpl @@ -4,9 +4,9 @@ go 1.27.3 // Pinned by the solution-go agent, so a new solution does not have to work the // graph out. The runtime pin is a RELEASE, never a pseudo-version: a -// pseudo-version names a commit that may never reach a tag, and -// obin-ai/core-solutions' check-consumed-apis gate requires the native -// projected-token runtime (v0.0.12 or later). +// pseudo-version names a commit that may never reach a tag, and a solution +// backend must pin the native projected-token runtime — v0.0.12 or later — +// which is what every consumer's consumed-API gate checks for. // // This block is `go mod tidy`-clean as generated, with go.sum beside it, so a // freshly scaffolded backend builds with the default -mod=readonly and no diff --git a/templates/page/rsbuild.config.ts.tmpl b/templates/page/rsbuild.config.ts.tmpl index 9af0036..cd16e67 100644 --- a/templates/page/rsbuild.config.ts.tmpl +++ b/templates/page/rsbuild.config.ts.tmpl @@ -21,8 +21,12 @@ const hostSingleton = { singleton: true, requiredVersion: false } as const; // and a second copy of a plugin entrypoint is a second React context. const hostProvided = { ...hostSingleton, import: false } as const; +// A share key is an EXACT module specifier, so every key here is spelled as +// one — quoted, even where JavaScript would accept a bare identifier. A bare +// `react` and a quoted "react" mean the same thing to a bundler and two +// different things to a reader skimming for a missing entry. const shared = { - react: { ...hostSingleton }, + "react": { ...hostSingleton }, "react/jsx-runtime": { ...hostSingleton }, "react-dom": { ...hostSingleton }, // A share key is an EXACT specifier. A kit that mounts its own root with From d071800a7072fa99ec84a8098b5ae218067dc667 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:24:51 -0400 Subject: [PATCH 3/6] fix: a solution that consumes anything was born red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every test in this package read a template; none ran what the templates render. So this adds the test that does — scaffold a solution the way `codefly add solution` scaffolds one, complete it the way the CLI completes it, compile it, and run the suite it ships with — and fixes the two defects it found immediately. The first stopped the boot. The solution runtime refuses a consumed module that declares no method ("consumed module %q declares no methods", passthrough.go), and the scaffolded consumes.go declared one per api.consumes entry whether or not anyone had composed its methods yet. A solution scaffolded with `--consumes` therefore could not render its interface document and could not start: the comment claiming an empty declaration means "nothing of it is reachable" described an intent the runtime does not share. consumed() now leaves an uncomposed module out, which says the same thing and starts. The second was an invariant the reference solution does not hold. The scaffolded drift check asserted consumed() equals api.consumes exactly, but api.consumes names every module a solution reaches ANYWHERE — the page reaches modules at the authority's edge with their own clients, never through the backend — and obin-ai/core-solutions' wiki declares six consumed modules and relays two. The check is split into the two directions that are actually true: consumed_gen.go must equal api.consumes (that IS the `codefly sync` drift check, and it is the generated file that must match), while this backend may relay any subset of it and nothing outside it. Both states of `codefly add solution` are now covered, with and without `--consumes`, and both build with the default -mod=readonly and pass their own gates on a tree no generator has run in. Removing the filter above turns the new test red, which is how it was checked. The service manifest the end-to-end test writes is lifted from the scaffolded GETTING_STARTED.md's own fenced example rather than written in the test, so what it proves is that the DOCUMENTED manifest is the one that works. Co-Authored-By: Claude Opus 5 --- endtoend_test.go | 112 ++++++++++++++++++++++++ templates/factory/consumes.go.tmpl | 24 ++++- templates/factory/consumes_test.go.tmpl | 67 ++++++++++---- 3 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 endtoend_test.go diff --git a/endtoend_test.go b/endtoend_test.go new file mode 100644 index 0000000..a9dc0c4 --- /dev/null +++ b/endtoend_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// THE VERIFICATION BAR, run rather than asserted. +// +// Every other test in this package reads a template. This one scaffolds a +// solution the way `codefly add solution` scaffolds one, completes it the way +// the CLI completes it, and then COMPILES it and runs the suite it ships +// with. A template that renders into Go that does not build, or into a tree +// whose own gates are red the moment it lands, passes every other test here +// and fails this one. +// +// It is what caught both defects this file was added with: a solution that +// consumed anything was born red, because the runtime refuses a consumed +// module declaring no method, and the scaffolded drift check asserted an +// equality the reference solution does not hold (api.consumes names every +// module the solution reaches anywhere; this backend relays a subset). + +// TestAScaffoldedSolutionBuildsAndPassesItsOwnGates is that run, for a +// solution that consumes modules and one that consumes none. Both are real +// states of `codefly add solution`: with `--consumes` and without. +func TestAScaffoldedSolutionBuildsAndPassesItsOwnGates(t *testing.T) { + for name, solutionManifest := range map[string]string{ + "consumes nothing": emptyManifest, + "consumes modules": consumingManifest, + } { + t.Run(name, func(t *testing.T) { + goTool := goToolchain(t) + backend := filepath.Join(scaffoldInto(t, "notes", solutionManifest), "services", backendService) + completeAsTheCLIDoes(t, backend) + + // `go build` with the default -mod=readonly and no proxy: the + // scaffolded go.mod and go.sum are tidy as generated, so a fresh + // backend builds without a tidy and without reaching the network + // for anything the agent's own module graph has not already + // resolved. + run(t, goTool, backend, "build", "./...") + // The suite the solution ships with: its consumption drift check, + // its own refusal of a module edge (SP-SOL-04), and the interface + // document, which must already be what `go generate ./...` would + // render — nobody has run a generator in this tree. + run(t, goTool, backend, "test", "./...") + }) + } +} + +// completeAsTheCLIDoes writes the two files `codefly add solution` writes into +// the service directory AFTER this agent's Create returns, and that this agent +// must not write (shape_test.go holds it to that): +// +// - solution.frontend.codefly.yaml, which the scaffolded main.go embeds. A +// backend does not compile without it, by design: the runtime serves this +// block and the CLI delivers it in the presence document it signs, so one +// file is the only way the served manifest and the signed presence cannot +// disagree. +// - the rest of service.codefly.yaml. It is taken from the scaffolded +// GETTING_STARTED.md's own example rather than written here, so what this +// test proves is that THE DOCUMENTED MANIFEST is the one that works. An +// example that drifts from what the CLI writes fails here. +func completeAsTheCLIDoes(t *testing.T, backend string) { + t.Helper() + require.NoError(t, os.WriteFile(filepath.Join(backend, "solution.frontend.codefly.yaml"), + []byte("title: Notes\norder: 60\nexposed_module: ./Page\nhost_contract: 1\n"), 0o600)) + + // What `codefly add service` writes before the solution's own block: the + // identity and the source convention. `spec.source-dir: .` is this agent's + // (service.go says why the module is at the service root). + base := "kind: service\nname: " + backendService + "\nversion: 0.0.0\n" + + "agent:\n publisher: " + agent.Publisher + "\n kind: codefly:service\n" + + " name: " + agent.Name + "\n version: " + agent.Version + "\n" + + "spec:\n source-dir: " + solutionSourceDir + "\n" + declared := fencedYAML(t, read(t, filepath.Join(backend, "GETTING_STARTED.md"))) + require.NoError(t, os.WriteFile(filepath.Join(backend, "service.codefly.yaml"), + []byte(base+declared+"\n"), 0o600)) +} + +// goToolchain is the `go` to build the scaffolded module with, or a skip. The +// agent itself is a Go program, so a tree that can run this package has one; +// the skip is for a sandbox that has stripped PATH. +func goToolchain(t *testing.T) string { + t.Helper() + tool, err := exec.LookPath("go") + if err != nil { + t.Skip("no go toolchain on PATH to build the scaffolded solution with") + } + return tool +} + +// run is one go command in the scaffolded backend, with its full output on +// failure: what failed to compile is the finding, not the exit status. +// +// GOWORK=off keeps the scaffolded module out of any go.work the agent's own +// checkout sits in — it is a separate module in a temporary directory, and a +// workspace would try to resolve it against this repository's. +func run(t *testing.T, goTool, dir string, args ...string) { + t.Helper() + command := exec.Command(goTool, args...) + command.Dir = dir + command.Env = append(os.Environ(), "GOWORK=off") + output, err := command.CombinedOutput() + require.NoErrorf(t, err, "`go %s` in the scaffolded backend:\n%s", + strings.Join(args, " "), output) +} diff --git a/templates/factory/consumes.go.tmpl b/templates/factory/consumes.go.tmpl index 4a6122b..f0f63c8 100644 --- a/templates/factory/consumes.go.tmpl +++ b/templates/factory/consumes.go.tmpl @@ -22,12 +22,32 @@ import ( // module methods it may call, which are constants of that module's generated // Go client. // -// A module with no entry in `composition` below is declared with no method, so -// nothing of it is reachable. That is the scaffolded state. +// A MODULE WITH NO COMPOSED METHOD IS NOT DECLARED HERE AT ALL, and the +// difference is not cosmetic: the runtime refuses a consumed module that +// declares no method ("consumed module %q declares no methods") and that +// refusal stops the boot. Declaring an uncomposed entry as an empty module +// would therefore not mean "nothing of it is reachable" — it would mean the +// solution does not start. +// +// Skipping it says the same thing the empty declaration was reaching for: +// nothing of that module is reachable through this backend. Two different +// things arrive here that way, and both are meant to: +// +// - a module whose methods nobody has composed YET. It is in api.consumes, +// so the composition delivers its binding; it reaches nothing until +// someone fills `composition` in below. A freshly scaffolded solution is +// entirely in this state, and starts. +// - a module this backend never relays. api.consumes names every module the +// solution reaches ANYWHERE, and a module the page calls at the +// authority's edge is not relayed through here. The reference solution +// declares six and relays two. func consumed() []solution.ConsumedModule { modules := make([]solution.ConsumedModule, 0, len(consumedAsAll)) for _, as := range consumedAsAll { declared := composition[as] + if len(declared.Methods) == 0 { + continue + } modules = append(modules, solution.ConsumedModule{ As: as, Scopes: declared.Scopes, diff --git a/templates/factory/consumes_test.go.tmpl b/templates/factory/consumes_test.go.tmpl index 9bddc99..dc0a947 100644 --- a/templates/factory/consumes_test.go.tmpl +++ b/templates/factory/consumes_test.go.tmpl @@ -17,28 +17,63 @@ const solutionManifestPath = "../../solution.codefly.yaml" // serviceManifestPath is this service's manifest, beside this file. const serviceManifestPath = "service.codefly.yaml" -// TestConsumedMatchesTheManifest is the static half of a check that otherwise -// only shows up at a boot nobody runs until the page is broken. +// TestTheGeneratedConsumptionMatchesTheManifest is THE DRIFT CHECK, and the +// reason consumption is generated rather than written twice. // -// consumed_gen.go is generated from api.consumes by `codefly sync`, and -// consumed() is derived from it, so the two cannot name different modules. -// What CAN go stale is the generated file itself: a module added to -// api.consumes reaches nothing until `codefly sync` runs. This fails until it -// has. -func TestConsumedMatchesTheManifest(t *testing.T) { - declared := []string{} - for _, module := range consumed() { - declared = append(declared, module.As) - } - slices.Sort(declared) +// consumed_gen.go is generated from api.consumes by `codefly sync`. It must +// name exactly what the manifest names: a module added to api.consumes reaches +// nothing until `codefly sync` has run, and that is invisible until the page +// calls it. This fails until it has run. +// +// It is the GENERATED file that is compared, not consumed() — see the two +// tests below for why those are different sets. +func TestTheGeneratedConsumptionMatchesTheManifest(t *testing.T) { + generated := append([]string{}, consumedAsAll...) + slices.Sort(generated) wanted := append([]string{}, manifestAliases(t)...) slices.Sort(wanted) - if !slices.Equal(declared, wanted) { - t.Fatalf("this backend consumes %v; %s declares %v.\n"+ + if !slices.Equal(generated, wanted) { + t.Fatalf("consumed_gen.go names %v; %s declares %v.\n"+ "consumed_gen.go is generated from api.consumes: run `codefly sync` and commit it.", - declared, solutionManifestPath, wanted) + generated, solutionManifestPath, wanted) + } +} + +// TestThisBackendRelaysOnlyWhatTheManifestDeclares is the direction that must +// hold absolutely: a module this backend relays but the manifest does not +// declare has no delivered binding, so the runtime refuses the boot naming it. +// +// THE REVERSE IS NOT A FAILURE, and that is the point of having two tests. +// api.consumes names every module the solution reaches ANYWHERE — the page +// reaches modules at the authority's edge with their own clients, and those +// are never relayed through this backend. The reference solution declares six +// consumed modules and relays two. An entry this backend does not relay is +// also the state of every entry in a freshly scaffolded solution, until +// someone composes its methods in consumes.go. +func TestThisBackendRelaysOnlyWhatTheManifestDeclares(t *testing.T) { + declared := map[string]bool{} + for _, as := range consumedAsAll { + declared[as] = true + } + for _, module := range consumed() { + if !declared[module.As] { + t.Errorf("this backend relays %q, which %s does not declare: the composition delivers no binding for it and the runtime refuses the boot naming it", + module.As, solutionManifestPath) + } + } +} + +// TestEveryRelayedModuleDeclaresAMethod: the runtime refuses a consumed module +// that declares no method, and that refusal stops the boot. consumed() leaves +// an uncomposed module out for exactly that reason, so this fails only if that +// filter is ever removed — here, in `go test`, rather than at a boot. +func TestEveryRelayedModuleDeclaresAMethod(t *testing.T) { + for _, module := range consumed() { + if len(module.Methods) == 0 { + t.Errorf("%q is relayed but declares no method: the runtime refuses a consumed module with none and the boot fails", module.As) + } } } From 09d865e20ac1407aa7af458432890810f2c671ec Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:29:15 -0400 Subject: [PATCH 4/6] feat: the release path, and the tests that pin it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent.codefly.yaml declared `release.workflow: releaser.yml` and .goreleaser.yaml's comment cited a release_test.go. Neither existed: a `v*` tag would have published nothing, so the agent could not be installed at all. The workflow is core's shape, from codefly-dev/service-go-grpc: the suite runs on the tag first and a release waits on it, then goreleaser-cross publishes with Syft mounted for the per-archive SBOMs. CI reuses core's own go-service-ci reusable workflow pinned at the core version go.mod requires, rather than restating its checks here. release_test.go pins the install path against CORE'S OWN registration rather than against a repeated string: resources.AgentKindRegistrationFor gives the executable name every build must produce and GitHubAsset gives the archive core fetches, and the GoReleaser template is resolved for a concrete platform and compared to it. A rename on either side now fails here instead of publishing an agent that downloads and cannot be found. It also pins the self-hosting choice, which is kept. `source.agent: self` is the CLI's exact sentinel (cmd/agents/build.go isSelfHostedSourcePackager) and it imposes four preconditions before publish — a declared source.bootstrap, a semver version, and a publisher and name that are each one path component — all four now checked in `go test` rather than at a publish that has already tagged. bootstrap-agent is held to the three variables the publisher states the target with (CODEFLY_AGENT_OUTPUT, _OS, _ARCH) and to CGO, which core's tree-sitter source inspection cannot ship without. Verified against the CLI at HEAD rather than assumed: self-hosting is supported, and naming `codefly.dev/go:` instead would make every release of this agent wait on a bump of that one. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 18 +++ .github/workflows/releaser.yml | 58 +++++++ release_test.go | 276 +++++++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/releaser.yml create mode 100644 release_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..632edbf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: ci + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # core's own Go service-agent CI, not a copy of it. The pin is the core + # version go.mod requires, so the checks this agent is held to move with the + # core it compiles against rather than drifting in a workflow nobody reads. + ci: + uses: codefly-dev/core/.github/workflows/go-service-ci.yml@v0.19.10 diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml new file mode 100644 index 0000000..af9afce --- /dev/null +++ b/.github/workflows/releaser.yml @@ -0,0 +1,58 @@ +name: GoReleaser + +# The tag workflow is the SOLE publisher of this agent's artifacts +# (agent.codefly.yaml declares `release.owner: workflow`), so nothing but a +# `v*` tag produces a release and `codefly publish` never uploads from a +# laptop. +on: + push: + tags: + - 'v*' + +jobs: + # The suite runs before anything is published, on the tag itself: a tag that + # does not pass is a tag that publishes nothing. The end-to-end test here + # scaffolds a solution and compiles it, so this job is also what proves the + # templates in the tagged tree render into Go that builds. + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go.mod + cache: false + - run: go test -v ./... + + release: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + # Each published archive carries its own SBOM, which GoReleaser needs + # Syft on PATH to produce. It is installed here and mounted into the + # cross-build container below, because that container has no Syft of its + # own. + - name: Install Syft for archive SBOMs + id: syft + uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + with: + syft-version: v1.44.0 + # CGO-capable, and that is not optional: core's authoritative source + # inspection links real tree-sitter grammars, so an agent built with + # CGO_ENABLED=0 cannot parse anything. goreleaser-cross is what supplies + # the darwin and linux cross compilers .goreleaser.yaml names. + - name: Run CGO-capable GoReleaser + run: >- + docker run --rm + -e GITHUB_TOKEN + -v "$GITHUB_WORKSPACE:/workspace" + -v "$SYFT_PATH:/usr/local/bin/syft:ro" + -w /workspace + ghcr.io/goreleaser/goreleaser-cross:v1.27.1-1 + release --clean + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + SYFT_PATH: ${{ steps.syft.outputs.cmd }} diff --git a/release_test.go b/release_test.go new file mode 100644 index 0000000..51558d1 --- /dev/null +++ b/release_test.go @@ -0,0 +1,276 @@ +package main + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/codefly-dev/core/resources" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// HOW THIS AGENT REACHES A HOST, pinned where a change to it is visible. +// +// An agent is installed by core downloading one archive from one GitHub +// release and running one executable out of it. Every name in that path is +// core's to decide, and .goreleaser.yaml restates all of them — so a rename +// here, or a GoReleaser default that moves, publishes an agent that downloads +// and then cannot be found. These tests compare against core's own +// AgentKindRegistration rather than against a spelling repeated in a string. + +// agentManifest is agent.codefly.yaml's publication half: the keys the CLI's +// `codefly publish` reads (cmd/agents/build.go), which resources.Agent — what +// main.go loads for the identity — does not carry. +type agentManifest struct { + Publisher string `yaml:"publisher"` + Kind string `yaml:"kind"` + Name string `yaml:"name"` + Version string `yaml:"version"` + Release struct { + Owner string `yaml:"owner"` + Workflow string `yaml:"workflow"` + } `yaml:"release"` + Source struct { + Directory string `yaml:"directory"` + Agent string `yaml:"agent"` + Bootstrap []string `yaml:"bootstrap"` + } `yaml:"source"` +} + +// stringOrSlice is a GitHub Actions field that accepts either one value or a +// list of them, such as `needs`. Both spellings mean the same thing, so both +// decode to the same slice rather than to a parse error on whichever this +// repository did not happen to use. +type stringOrSlice []string + +func (s *stringOrSlice) UnmarshalYAML(node *yaml.Node) error { + if node.Kind == yaml.ScalarNode { + var one string + if err := node.Decode(&one); err != nil { + return err + } + *s = stringOrSlice{one} + return nil + } + var many []string + if err := node.Decode(&many); err != nil { + return err + } + *s = many + return nil +} + +func readYAML(t *testing.T, path string, target any) { + t.Helper() + payload, err := os.ReadFile(path) //nolint:gosec // a path spelled in this test + require.NoError(t, err) + require.NoError(t, yaml.Unmarshal(payload, target), "%s must be readable YAML", path) +} + +// TestTheArchiveIsTheOneCoreDownloads is the contract with no second copy: +// core builds the asset name it fetches and the executable it runs from the +// agent's KIND and NAME, so both are derived here from core's own +// registration and compared to what GoReleaser is told to produce. +// +// A GoReleaser archive is ".", so the template carries +// everything but the extension. +func TestTheArchiveIsTheOneCoreDownloads(t *testing.T) { + var manifest agentManifest + readYAML(t, "agent.codefly.yaml", &manifest) + + registration, err := resources.AgentKindRegistrationFor(resources.AgentKind(manifest.Kind)) + require.NoErrorf(t, err, "core registers no agent kind %q", manifest.Kind) + + var config struct { + ProjectName string `yaml:"project_name"` + Builds []struct { + ID string `yaml:"id"` + Binary string `yaml:"binary"` + GOOS []string `yaml:"goos"` + GOARCH []string `yaml:"goarch"` + } `yaml:"builds"` + Archives []struct { + Formats []string `yaml:"formats"` + NameTemplate string `yaml:"name_template"` + } `yaml:"archives"` + } + readYAML(t, ".goreleaser.yaml", &config) + + // The executable core runs out of the archive. Every build must produce + // it: core runs one name whatever platform it downloaded. + executable := registration.ExecutableName(manifest.Name) + require.NotEmpty(t, config.Builds, "an agent that builds nothing publishes nothing") + for _, build := range config.Builds { + require.Equalf(t, executable, build.Binary, + "build %q produces %q; core runs %q", build.ID, build.Binary, executable) + } + + // The archive core fetches, rendered from the same registration. The + // GoReleaser template is resolved here for one concrete platform, so the + // comparison is against a real name rather than against another template. + require.Len(t, config.Archives, 1, "core downloads one archive per platform") + archive := config.Archives[0] + require.Equal(t, []string{"tar.gz"}, archive.Formats) + + const version = "0.0.0" + rendered := strings.NewReplacer( + "{{ .ProjectName }}", config.ProjectName, + "{{ .Version }}", version, + "{{ .Os }}", runtime.GOOS, + "{{ .Arch }}", runtime.GOARCH, + ).Replace(archive.NameTemplate) + "." + archive.Formats[0] + require.NotContains(t, rendered, "{{", "the archive name template uses a value this test does not resolve") + require.Equal(t, registration.GitHubAsset(manifest.Name, version, runtime.GOOS, runtime.GOARCH), rendered, + "the published archive is not the one core downloads") +} + +// TestEveryPublishedPlatformIsBuiltWithCGO. core's authoritative source +// inspection links real tree-sitter grammars, so an agent built with +// CGO_ENABLED=0 loads and then cannot parse anything — a failure that looks +// like a bad service, not a bad build. The cross compilers are why the release +// job runs inside goreleaser-cross. +func TestEveryPublishedPlatformIsBuiltWithCGO(t *testing.T) { + var config struct { + Builds []struct { + ID string `yaml:"id"` + Env []string `yaml:"env"` + } `yaml:"builds"` + } + readYAML(t, ".goreleaser.yaml", &config) + for _, build := range config.Builds { + require.Containsf(t, build.Env, "CGO_ENABLED=1", + "build %q disables CGO, so core's tree-sitter source inspection cannot ship", build.ID) + } +} + +// TestTheTagWorkflowIsTheSolePublisher, and carries an SBOM per archive. +// +// `release.owner: workflow` says no artifact is published except by the +// workflow a `v*` tag starts, so the workflow must exist and must actually +// release. `release.prerelease: auto` is what keeps a `codefly publish dev` +// build (v-dev.) off Latest, so `version: latest` can never +// resolve to a dev build. +func TestTheTagWorkflowIsTheSolePublisher(t *testing.T) { + var manifest agentManifest + readYAML(t, "agent.codefly.yaml", &manifest) + require.Equal(t, "workflow", manifest.Release.Owner, "the tag workflow must be the sole artifact publisher") + require.Equal(t, "releaser.yml", manifest.Release.Workflow) + + var config struct { + Release struct { + Prerelease string `yaml:"prerelease"` + } `yaml:"release"` + SBOMs []struct { + Artifacts string `yaml:"artifacts"` + Documents []string `yaml:"documents"` + Disable bool `yaml:"disable"` + } `yaml:"sboms"` + } + readYAML(t, ".goreleaser.yaml", &config) + require.Equal(t, "auto", config.Release.Prerelease, + "a dev build's prerelease tag must publish as a GitHub prerelease, never as Latest") + require.Len(t, config.SBOMs, 1) + require.Equal(t, "archive", config.SBOMs[0].Artifacts, "each published archive carries its own SBOM") + require.False(t, config.SBOMs[0].Disable) + require.Equal(t, []string{"${artifact}.sbom.json"}, config.SBOMs[0].Documents) + + var workflow struct { + On struct { + Push struct { + Tags []string `yaml:"tags"` + } `yaml:"push"` + } `yaml:"on"` + Jobs map[string]struct { + Needs stringOrSlice `yaml:"needs"` + Steps []struct { + ID string `yaml:"id"` + Uses string `yaml:"uses"` + Run string `yaml:"run"` + Env map[string]string `yaml:"env"` + } `yaml:"steps"` + } `yaml:"jobs"` + } + readYAML(t, filepath.Join(".github", "workflows", manifest.Release.Workflow), &workflow) + require.Equal(t, []string{"v*"}, workflow.On.Push.Tags, "only a version tag publishes") + + // The suite runs before anything is published: a tag that does not pass + // publishes nothing. + release, found := workflow.Jobs["release"] + require.True(t, found, "the release workflow declares no release job") + require.Contains(t, release.Needs, "test", "a release must wait on the suite") + + // Syft installed AND mounted into the cross-build container, which has + // none of its own — without the mount GoReleaser produces no SBOM and the + // `sboms` block above is silently inert. + installed, mounted := false, false + for _, step := range release.Steps { + if step.ID == "syft" && strings.HasPrefix(step.Uses, "anchore/sbom-action/download-syft@") { + installed = true + } + if strings.Contains(step.Run, `-v "$SYFT_PATH:/usr/local/bin/syft:ro"`) && + step.Env["SYFT_PATH"] == "${{ steps.syft.outputs.cmd }}" { + mounted = true + } + } + require.True(t, installed, "the release job installs no Syft, so no archive SBOM is produced") + require.True(t, mounted, "Syft is not mounted into the cross build, so the sboms block is inert") +} + +// TestSelfHostingDeclaresWhatThePublisherRequires. +// +// This agent packages its OWN source (`source.agent: self`) rather than naming +// another agent's release. That is a deliberate choice: naming +// `codefly.dev/go:` would make every release of this agent wait on a bump +// of that one, and this agent is a Go program that can build itself for a host +// with no codefly.dev service agent installed yet. +// +// The CLI enforces four preconditions on that choice before it will publish +// (cmd/agents/build.go, prepareAgentPackager). They are pinned here so a +// malformed manifest fails in `go test` rather than at a publish that has +// already tagged. +func TestSelfHostingDeclaresWhatThePublisherRequires(t *testing.T) { + var manifest agentManifest + readYAML(t, "agent.codefly.yaml", &manifest) + require.Equal(t, ".", manifest.Source.Directory, + "publication must select a source packager even when several compatible agents are installed") + if manifest.Source.Agent != "self" { + t.Skipf("this agent is packaged by %q, not self-hosted", manifest.Source.Agent) + } + + // 1. a self-hosted agent must declare the command that builds it, and + // 2. that command must exist and be executable. + require.NotEmpty(t, manifest.Source.Bootstrap, "a self-hosted source agent must declare source.bootstrap") + bootstrap := manifest.Source.Bootstrap[0] + require.NotEmpty(t, strings.TrimSpace(bootstrap), "the source bootstrap command is required") + info, err := os.Stat(strings.TrimPrefix(bootstrap, "./")) + require.NoErrorf(t, err, "source.bootstrap names %s, which is not in this repository", bootstrap) + require.NotZerof(t, info.Mode().Perm()&0o111, "%s is not executable, so the publisher cannot run it", bootstrap) + + // 3. the version must be a semver release: it becomes the canonical + // artifact version the self-built packager is installed under. + require.Regexp(t, `^\d+\.\d+\.\d+`, manifest.Version, + "a self-hosted source agent must declare its canonical artifact version") + + // 4. publisher and name each become ONE path component of the directory + // the packager is installed into, so neither may contain a separator. + for _, component := range []string{manifest.Publisher, manifest.Name} { + require.NotContains(t, component, "/", + "a self-hosted agent's publisher and name must be single path components") + require.NotContains(t, component, `\`) + require.NotEqual(t, ".", component) + } + + // The bootstrap builds for the host the publisher is running on, which it + // learns from these three variables and nothing else. + script, err := os.ReadFile(strings.TrimPrefix(bootstrap, "./")) //nolint:gosec // a path the manifest names + require.NoError(t, err) + for _, supplied := range []string{"CODEFLY_AGENT_OUTPUT", "CODEFLY_AGENT_OS", "CODEFLY_AGENT_ARCH"} { + require.Containsf(t, string(script), supplied, + "%s ignores %s, which is how the publisher states what to build and where to put it", bootstrap, supplied) + } + require.Contains(t, string(script), "CGO_ENABLED=1", + "the self-built packager must keep CGO enabled, as every released build does") +} From 2c2f241c4f78572da8ec1756b61254c4c7bb1683 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:47:22 -0400 Subject: [PATCH 5/6] refactor: author the agent as base/, generate templates/factory from it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The templates were hand-written. That is the wrong authoring model for a service agent and it is why the two defects in the previous commit existed at all: a template cannot be compiled, vetted or tested, so a scaffold that does not build looks exactly like one that does. Every service agent holds a REAL service in base/ and generates templates/factory from it with core's generator.GenerateServiceTemplate (`codefly agent generate --service .`), which copies base/, adds a .tmpl suffix and applies the literal replacements in base/service.generation.codefly.yaml. codefly-dev/service-go-grpc is the proof. This repository now does the same. base/ is a real solution backend called `codefly-base`: it compiles with the default -mod=readonly, it vets, and ITS OWN SUITE IS THE SUITE EVERY SCAFFOLDED SOLUTION SHIPS WITH. It is staged into the layout a real solution has — /services/backend beside /solution.codefly.yaml — because that is where its consumes_test.go really finds the manifest, at ../../, which from /base would be outside this repository. It deliberately consumes one module and composes no methods for it. That is the state that was broken: the runtime refuses a consumed module declaring no method, so base/ is permanently in the shape whose regression would be caught. templates/factory is now generated, and the three replacements reproduced the hand-written tree byte for byte except where the move was a deliberate simplification: - {{ .Module.Name.LowerCase }} became {{ .SolutionID }}, the same value. - {{ .InterfaceDescription }} became the prose it rendered to, so the description lives once, in base/openapi.go, instead of in a Go function the template referred to. - {{ .Service.Name.LowerCase }} became `backend`. A solution's entry service is named `backend` by the shape's definition (the CLI's solutionEntryService and this agent's backendService are that constant), the scaffolded go.mod already hard-codes the `/backend` module suffix the compiled binary is named after, and the word appears in prose on nearly every line, which a whole-file substitution would corrupt. The five files base/ needs to be a real service but that must never be scaffolded are listed under `ignores`: the three documents the CLI owns (service.codefly.yaml, solution.frontend.codefly.yaml) plus the two the agent renders itself (consumed_gen.go, openapi/api.swagger.json). A template for any of them would be a second mechanism for one file. Core's own generator has a hardcoded exclusion for service.codefly.yaml, but it compares a full path and never fires, so the list is what actually holds. The drift check calls core's generator directly rather than shelling out to the CLI, so it needs no second binary and holds the templates to the exact code `codefly agent generate` runs. It compares whole trees in both directions, because regeneration overwrites a file but never deletes one — a template whose source left base/ would otherwise be scaffolded forever. Both directions were checked: hand-editing a template fails it, and editing base/ then regenerating carries the change through. `go test -update` is one flag for base/'s derived files and the templates together, so the two cannot be refreshed one without the other. AGENTS.md states the model, because the mistake this commit undoes is one a reader makes by default. templates/page stays hand-written and says why, in AGENTS.md and at main.go's pageFS: GenerateServiceTemplate produces templates/factory from base/ and nothing else, and the page is written to a SIBLING of the service directory, so there is no base/ subdirectory it could come from. 29 tests pass, 0 skipped. A scaffolded solution still passes core-solutions' check-boundaries, check-consumed-apis and check-solution-mesh at exit 0. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 113 +++++++++ base/.gitignore | 14 ++ base/GETTING_STARTED.md | 69 ++++++ base/consumed_gen.go | 19 ++ base/consumes.go | 118 ++++++++++ base/consumes_test.go | 164 +++++++++++++ base/go.mod | 80 +++++++ base/go.sum | 271 ++++++++++++++++++++++ base/main.go | 63 +++++ base/openapi.go | 28 +++ base/openapi/api.swagger.json | 39 ++++ base/openapi_test.go | 41 ++++ base/operations.go | 40 ++++ base/service.codefly.yaml | 62 +++++ base/service.generation.codefly.yaml | 79 +++++++ base/solution.frontend.codefly.yaml | 8 + base_test.go | 255 ++++++++++++++++++++ endtoend_test.go | 48 +--- factory_test.go | 153 ++++++++++++ main.go | 20 ++ scaffold.go | 9 +- shape_test.go | 39 +++- templates/factory/.gitignore.tmpl | 2 +- templates/factory/GETTING_STARTED.md.tmpl | 8 +- templates/factory/openapi.go.tmpl | 2 +- 25 files changed, 1684 insertions(+), 60 deletions(-) create mode 100644 AGENTS.md create mode 100644 base/.gitignore create mode 100644 base/GETTING_STARTED.md create mode 100644 base/consumed_gen.go create mode 100644 base/consumes.go create mode 100644 base/consumes_test.go create mode 100644 base/go.mod create mode 100644 base/go.sum create mode 100644 base/main.go create mode 100644 base/openapi.go create mode 100644 base/openapi/api.swagger.json create mode 100644 base/openapi_test.go create mode 100644 base/operations.go create mode 100644 base/service.codefly.yaml create mode 100644 base/service.generation.codefly.yaml create mode 100644 base/solution.frontend.codefly.yaml create mode 100644 base_test.go create mode 100644 factory_test.go diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..04b7677 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,113 @@ +# Working in this repository + +`solution-go` is a `codefly:service` agent. It scaffolds a **solution +backend** — the entry service of a solution, built on +`github.com/codefly-dev/solution-runtime-go` — where the generic `go` agent +scaffolds a plain Go service. + +## base/ is hand-written. templates/factory/ is GENERATED. Never edit a template. + +This is the authoring model of every service agent, and the single most +important thing to know before changing anything here. + +``` +base/ a REAL, COMPILING, TESTED solution backend <- edit this +templates/factory/ generated from base/ <- never edit +``` + +`templates/factory/` is produced by core's +`generator.GenerateServiceTemplate` — `codefly agent generate --service .` — +which copies every file in `base/`, adds a `.tmpl` suffix, and applies the +literal replacements in `base/service.generation.codefly.yaml`. + +**Why, and not just convention.** A template cannot be compiled, vetted or +tested, so hand-written templates let you ship a scaffold that does not build +and nothing notices until someone scaffolds it. `base/` is a real service: +`go build`, `go vet` and `go test` run against it in CI +(`TestBaseCompilesAndPassesItsOwnSuite`), and **its own test suite is the suite +every scaffolded solution ships with**. The thing this agent scaffolds is +known-good before anybody scaffolds it. + +### To change what a solution is scaffolded with + +1. Edit `base/`. Keep it compiling: `cd base && GOWORK=off go build ./...`. +2. Regenerate and refresh the derived files: + ``` + GOWORK=off go test -update ./... + ``` + That one flag rewrites `base/consumed_gen.go`, `base/openapi/api.swagger.json` + and all of `templates/factory/`, so `base/` and the templates can never be + refreshed one without the other. +3. `GOWORK=off go test ./...` + +`TestFactoryTemplatesAreGeneratedFromBase` fails on any difference between +`templates/factory/` and what `base/` generates, in both directions — a stale +template whose source left `base/` is caught too, because regeneration +overwrites a file but never deletes one. An edit made to a template instead of +to `base/` is reverted by the next regeneration and fails the suite until it is. + +### The replacements are literal and whole-file + +`base/` is a real solution named `codefly-base`, spelled three ways, each +derived from the name exactly as `scaffold.go` derives it for a real solution: + +| in `base/` | becomes | derived by | +|---|---|---| +| `codefly-base` | `{{ .SolutionID }}` | `Module.Name.LowerCase` | +| `Codefly Base` | `{{ .Title }}` | `titleFrom` | +| `CodeflyBase` | `{{ .OperationPrefix }}` | `aliasField` | + +Order matters and no `from` may be a substring of an earlier `to`. +`TestGeneratedTemplatesCarryNoBaseSolutionName` fails if any spelling of +`codefly-base` survives into a template, which is what catches a new +capitalisation introduced in `base/` that no replacement covers. + +`backend` is **not** replaced. A solution's entry service is named `backend` by +the shape's own definition — the CLI's `solutionEntryService` and this agent's +`backendService` are both that constant — so it is not a parameter, and it +appears in prose on nearly every line (`a solution backend`, `the compiled +backend`) which a whole-file substitution would corrupt. + +## The other template trees are not generated + +| tree | generated? | why | +|---|---|---| +| `templates/factory/` | yes, from `base/` | the service directory | +| `templates/page/` | **no — hand-written** | the `./Page` npm remote is not a Go service, so `GenerateServiceTemplate` does not produce it, and it is written to a SIBLING directory (`services/fe-remote`) rather than into the service. See `main.go`'s `pageFS`. | +| `templates/builder/` | no | Dockerfile and dockerignore, rendered with the builder's own context | +| `templates/deployment/` | no | kustomize base and overlays, rendered with the deployment context | +| `templates/agent/` | no | this agent's own README, not part of a scaffolded tree | + +## What this agent must never scaffold + +`codefly add solution` (codefly-dev/cli#1216) writes the solution's +declarations **after** this agent's `Create` returns. Anything written here in +those places is overwritten silently, by landing first: + +- `module-identity`, the endpoints, and the only two `service-dependencies` a + solution backend has (the authority interface plus `authority/hop` as + `kind: external`); +- the five documents a solution is installed from. + +**In particular, never a `service-dependency` on a consumed module.** A +solution reaches every module through the authority's hop, which *checks* the +call; Codefly renders a `service-dependency` into a mesh allow rule straight to +that module's service, past the check, and `check_solution_mesh.py` refuses it +(SP-SOL-04). `shape_test.go` holds the agent to all of this, and the files in +`base/` that carry the CLI's declarations are listed under `ignores` in +`base/service.generation.codefly.yaml` so they are never templatized. + +## Build and test + +| what | command | +|---|---| +| the agent | `GOWORK=off go build ./... && GOWORK=off go vet ./...` | +| everything | `GOWORK=off go test ./...` | +| refresh generated files | `GOWORK=off go test -update ./...` | +| `base/` alone | `cd base && GOWORK=off go build ./...` | + +`base/` is a nested module, so the root `./...` does not include it; +`TestBaseCompilesAndPassesItsOwnSuite` builds, vets and tests it by staging it +into the layout a real solution has (`/services/backend` beside +`/solution.codefly.yaml`), which is where its `consumes_test.go` really +finds the solution manifest. diff --git a/base/.gitignore b/base/.gitignore new file mode 100644 index 0000000..ef7ed4e --- /dev/null +++ b/base/.gitignore @@ -0,0 +1,14 @@ +# The compiled backend +app +backend + +# Go workspace (keep the module-level go.mod pinned) +go.work +go.work.sum + +# Test binaries and coverage +*.test +*.out + +# Codefly local artifacts +.codefly/ diff --git a/base/GETTING_STARTED.md b/base/GETTING_STARTED.md new file mode 100644 index 0000000..da57edf --- /dev/null +++ b/base/GETTING_STARTED.md @@ -0,0 +1,69 @@ +# The codefly-base solution's backend + +Scaffolded by `codefly.dev/solution-go`. Two directories were written: + +- `services/backend` — this service, the solution's entry service. +- `services/fe-remote` — the `./Page` Module Federation remote. An npm package, + not a Codefly service, which is why nothing else writes it. + +## What is left, in order + +1. `cd services/backend && go build ./... && go test ./...` +2. `cd services/fe-remote && npm install && npm test && npm run typecheck` + The `@codefly-dev/*` kits come from GitHub Packages, so `NODE_AUTH_TOKEN` + must be set to a token with `read:packages` (see `.npmrc`). +3. Declare what the page may read, under `api.consumes` in the solution's + `solution.codefly.yaml`, then `codefly sync` to regenerate `consumed_gen.go` + and fill in that module's entry in `consumes.go`. + Add a binding to `module.contract.codefly.yaml` for each one, and have the + composition fill its two slots — `_ENDPOINT` and `_BINDING` in this + solution's own configuration group. The runtime opens a relay for every + declared module at boot and refuses the boot while a slot is empty. +4. Serve a route of your own: add a `solution.Operation` in `operations.go`, + then `go generate ./...` to re-render `openapi/api.swagger.json`. That + document is what `codefly generate contracts` publishes as this endpoint's + contract, and the CLI derives the rows the authority's Check applies from it. +5. Read a configuration value with `github.com/codefly-dev/sdk-go` + (`codefly.For(ctx).WorkspaceConfiguration(group, key)`) and add it to + `go.mod` when you do. Nothing is scaffolded that reads one. + +## If this service was added with `codefly add service` rather than `codefly add solution` + +`codefly add solution` writes the five declarations a solution is installed +from and the shape of this service's manifest. Added on its own, this service +has none of them, and it needs all of this in `service.codefly.yaml`: + +```yaml +module-identity: true +service-dependencies: + - interface: codefly.dev/authority@>=0.2.0 <0.4.0 + - name: hop + module: authority + kind: external + endpoints: + - name: grpc +workspace-configuration-dependencies: + - authority + - codefly-base +endpoints: + - name: http + visibility: public + exposure: none + api: http + secured: false + health: + readiness: + kind: http + path: /health + statuses: ["200"] +``` + +Never add a `service-dependencies` entry for a module this solution consumes. +Every module is reached through `authority/hop`, which checks the call; a +direct edge renders into a mesh allow rule past that check. + +It also needs `solution.frontend.codefly.yaml` beside this file, which +`main.go` embeds, and the solution's own `solution.codefly.yaml`, +`module.contract.codefly.yaml` and `module.package.codefly.yaml` one directory +above `services/`. `codefly add solution --package-id / +--agent solution-go` writes all of them. diff --git a/base/consumed_gen.go b/base/consumed_gen.go new file mode 100644 index 0000000..41c305a --- /dev/null +++ b/base/consumed_gen.go @@ -0,0 +1,19 @@ +// Code generated from solution.codefly.yaml by the codefly.dev/solution-go +// agent. DO NOT EDIT. +// +// api.consumes is the one place this solution says which modules it may reach. +// These names are that list, so consumes.go cannot name a module the manifest +// does not — nor miss one it does. Regenerate it with `codefly sync`; +// consumes_test.go fails while this file and the manifest disagree. + +package main + +// consumedAs names each consumed module, so consumes.go spells no `as` by hand. +var consumedAs = struct { + Documents string +}{ + Documents: "documents", +} + +// consumedAsAll is the same list, in manifest order. +var consumedAsAll = []string{"documents"} diff --git a/base/consumes.go b/base/consumes.go new file mode 100644 index 0000000..f0f63c8 --- /dev/null +++ b/base/consumes.go @@ -0,0 +1,118 @@ +package main + +import ( + "strings" + + solution "github.com/codefly-dev/solution-runtime-go" +) + +// consumed is every module call this solution's page may make through this +// backend, and nothing else of any module is reachable from it. Each call is +// relayed by the runtime's passthrough to the authority's hop in front of the +// module, AS THE VIEWER — a child of the person's Work Context, issued through +// this solution's binding for that module (module.contract.codefly.yaml) — and +// the module authorizes it. This declaration only narrows what may be asked +// and what comes back. +// +// THE MODULES ARE NOT NAMED HERE. They come from consumedAsAll, which is +// generated from api.consumes in the solution's solution.codefly.yaml: that +// manifest is the one place this solution says what it may reach, and a second +// list here is a second declaration that drifts. What this file adds is the +// part no manifest can carry — the authority each call asks for, and the +// module methods it may call, which are constants of that module's generated +// Go client. +// +// A MODULE WITH NO COMPOSED METHOD IS NOT DECLARED HERE AT ALL, and the +// difference is not cosmetic: the runtime refuses a consumed module that +// declares no method ("consumed module %q declares no methods") and that +// refusal stops the boot. Declaring an uncomposed entry as an empty module +// would therefore not mean "nothing of it is reachable" — it would mean the +// solution does not start. +// +// Skipping it says the same thing the empty declaration was reaching for: +// nothing of that module is reachable through this backend. Two different +// things arrive here that way, and both are meant to: +// +// - a module whose methods nobody has composed YET. It is in api.consumes, +// so the composition delivers its binding; it reaches nothing until +// someone fills `composition` in below. A freshly scaffolded solution is +// entirely in this state, and starts. +// - a module this backend never relays. api.consumes names every module the +// solution reaches ANYWHERE, and a module the page calls at the +// authority's edge is not relayed through here. The reference solution +// declares six and relays two. +func consumed() []solution.ConsumedModule { + modules := make([]solution.ConsumedModule, 0, len(consumedAsAll)) + for _, as := range consumedAsAll { + declared := composition[as] + if len(declared.Methods) == 0 { + continue + } + modules = append(modules, solution.ConsumedModule{ + As: as, + Scopes: declared.Scopes, + Methods: declared.Methods, + Authority: relayFor(as), + }) + } + return modules +} + +// consumedComposition is what this solution asks for at one module: the +// authority it asks the authority to issue for the viewer, and the methods it +// may call. A method that declares its own Scopes asks only for those. +type consumedComposition struct { + Scopes []solution.Scope + Methods []solution.ConsumedMethod +} + +// composition is that, per module, keyed by the `as` api.consumes gives it. +// Add the module's Go client to go.mod and name its procedures from the +// generated package, never as string literals — a renamed procedure must fail +// to compile here rather than 404 at the hop: +// +// var composition = map[string]consumedComposition{ +// consumedAs.Documents: { +// Scopes: []solution.Scope{ +// {ResourceKind: "documents", Actions: []string{"read"}}, +// {ResourceKind: "documents.collection", Actions: []string{"read"}}, +// }, +// Methods: []solution.ConsumedMethod{ +// {Name: documentsv1connect.DocumentsServiceListCollectionProcedure, +// Response: solution.MustFieldMask(&documentsv1.ListCollectionResponse{}, "documents.entry.entry_id", "documents.entry.path")}, +// }, +// }, +// } +// +// Response is an ALLOWLIST, named leaf by leaf, so a field the module adds +// later is dropped until someone names it. Omitting it sends the whole +// message to the page. +var composition = map[string]consumedComposition{} + +// slotsGroup is this solution's own workspace configuration group: where the +// composition says, per consumed module, where it is and which of this +// solution's bindings the viewer's calls are issued through. +const slotsGroup = solutionID + +// relayFor is those two slots for one consumed module. +// +// They are the COMPOSITION's to fill, never this repository's: `_ENDPOINT` +// holds the grpc address of the hop in front of the module (an +// ${endpoint:authority/hop/grpc} reference the run resolves) and `_BINDING` +// the id the composition installs this solution's binding for that module +// under. The runtime opens a relay for every declared module at boot and +// refuses the boot, by name, while either is empty — which is the right +// moment to find out. +func relayFor(as string) solution.AuthorityRelay { + return solution.AuthorityRelay{ + Group: slotsGroup, + EndpointKey: slotKey(as, "ENDPOINT"), + BindingKey: slotKey(as, "BINDING"), + } +} + +// slotKey is the configuration key an `as` yields: "document-store" and +// "ENDPOINT" give DOCUMENT_STORE_ENDPOINT. +func slotKey(as, suffix string) string { + return strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(as)) + "_" + suffix +} diff --git a/base/consumes_test.go b/base/consumes_test.go new file mode 100644 index 0000000..dc0a947 --- /dev/null +++ b/base/consumes_test.go @@ -0,0 +1,164 @@ +package main + +import ( + "os" + "path/filepath" + "slices" + "testing" + + "gopkg.in/yaml.v3" +) + +// solutionManifestPath is the solution's own manifest: api.consumes lives +// there, one directory above services/, where `codefly add solution` writes it +// and where the platform's gates read it. +const solutionManifestPath = "../../solution.codefly.yaml" + +// serviceManifestPath is this service's manifest, beside this file. +const serviceManifestPath = "service.codefly.yaml" + +// TestTheGeneratedConsumptionMatchesTheManifest is THE DRIFT CHECK, and the +// reason consumption is generated rather than written twice. +// +// consumed_gen.go is generated from api.consumes by `codefly sync`. It must +// name exactly what the manifest names: a module added to api.consumes reaches +// nothing until `codefly sync` has run, and that is invisible until the page +// calls it. This fails until it has run. +// +// It is the GENERATED file that is compared, not consumed() — see the two +// tests below for why those are different sets. +func TestTheGeneratedConsumptionMatchesTheManifest(t *testing.T) { + generated := append([]string{}, consumedAsAll...) + slices.Sort(generated) + + wanted := append([]string{}, manifestAliases(t)...) + slices.Sort(wanted) + + if !slices.Equal(generated, wanted) { + t.Fatalf("consumed_gen.go names %v; %s declares %v.\n"+ + "consumed_gen.go is generated from api.consumes: run `codefly sync` and commit it.", + generated, solutionManifestPath, wanted) + } +} + +// TestThisBackendRelaysOnlyWhatTheManifestDeclares is the direction that must +// hold absolutely: a module this backend relays but the manifest does not +// declare has no delivered binding, so the runtime refuses the boot naming it. +// +// THE REVERSE IS NOT A FAILURE, and that is the point of having two tests. +// api.consumes names every module the solution reaches ANYWHERE — the page +// reaches modules at the authority's edge with their own clients, and those +// are never relayed through this backend. The reference solution declares six +// consumed modules and relays two. An entry this backend does not relay is +// also the state of every entry in a freshly scaffolded solution, until +// someone composes its methods in consumes.go. +func TestThisBackendRelaysOnlyWhatTheManifestDeclares(t *testing.T) { + declared := map[string]bool{} + for _, as := range consumedAsAll { + declared[as] = true + } + for _, module := range consumed() { + if !declared[module.As] { + t.Errorf("this backend relays %q, which %s does not declare: the composition delivers no binding for it and the runtime refuses the boot naming it", + module.As, solutionManifestPath) + } + } +} + +// TestEveryRelayedModuleDeclaresAMethod: the runtime refuses a consumed module +// that declares no method, and that refusal stops the boot. consumed() leaves +// an uncomposed module out for exactly that reason, so this fails only if that +// filter is ever removed — here, in `go test`, rather than at a boot. +func TestEveryRelayedModuleDeclaresAMethod(t *testing.T) { + for _, module := range consumed() { + if len(module.Methods) == 0 { + t.Errorf("%q is relayed but declares no method: the runtime refuses a consumed module with none and the boot fails", module.As) + } + } +} + +// TestEveryConsumedModuleIsReachedThroughTheHop pins that every module call +// goes to the authority's hop at an address and a binding THE COMPOSITION +// supplies, never to an address this repository names. A declaration missing +// either slot is a boot that fails naming it, so this is the same refusal one +// step earlier. +func TestEveryConsumedModuleIsReachedThroughTheHop(t *testing.T) { + for _, module := range consumed() { + relay := module.Authority + if relay.Group == "" || relay.EndpointKey == "" || relay.BindingKey == "" { + t.Errorf("%q names no hop slots (group=%q endpoint=%q binding=%q): the composition says where a module is and which binding reaches it", + module.As, relay.Group, relay.EndpointKey, relay.BindingKey) + } + } +} + +// TestNoModuleEdgeIsDeclared is SP-SOL-04 in this solution's own words: a +// solution reaches every module through the authority's hop, which checks the +// call, and a `service-dependencies` edge on a module renders into a mesh +// allow rule straight to that module's service, PAST the check. +// +// The only edges this service may hold are the host's own door: the authority +// interface it requires by `interface:` alone, and `authority/hop`. The +// consumer repository's check-solution-mesh gate refuses anything else; this +// fails here first, in `go test`, where whoever added the edge is standing. +func TestNoModuleEdgeIsDeclared(t *testing.T) { + var document struct { + Dependencies []struct { + Interface string `yaml:"interface"` + Name string `yaml:"name"` + Module string `yaml:"module"` + } `yaml:"service-dependencies"` + } + content, err := os.ReadFile(serviceManifestPath) + if err != nil { + t.Fatalf("cannot read %s: %v", serviceManifestPath, err) + } + if err := yaml.Unmarshal(content, &document); err != nil { + t.Fatalf("cannot read %s: %v", serviceManifestPath, err) + } + for _, dependency := range document.Dependencies { + switch { + case dependency.Module == "": + // An interface requirement, or a dependency on this solution's own + // service. Neither names a module service. + case dependency.Module == "authority" && dependency.Name == "hop": + // The authority's door: it lets through only a call the authority + // checked. + default: + t.Errorf("%s declares a direct path to %s/%s. A solution reaches a module through the authority's hop, which checks the call; this edge renders into a mesh allow rule past it (SP-SOL-04). Declare the module under api.consumes instead.", + serviceManifestPath, dependency.Module, dependency.Name) + } + } +} + +// manifestAliases reads the `as` of every api.consumes entry. A missing +// manifest fails rather than reading as "consumes nothing": this service is a +// solution's entry service, and a solution without a manifest is not installed +// by anything. +func manifestAliases(t *testing.T) []string { + t.Helper() + content, err := os.ReadFile(filepath.FromSlash(solutionManifestPath)) + if err != nil { + t.Fatalf("cannot read %s, which every reader of this solution reads: %v", solutionManifestPath, err) + } + var document struct { + API struct { + Consumes []struct { + ID string `yaml:"id"` + As string `yaml:"as"` + } `yaml:"consumes"` + } `yaml:"api"` + } + if err := yaml.Unmarshal(content, &document); err != nil { + t.Fatalf("cannot read %s: %v", solutionManifestPath, err) + } + aliases := []string{} + for _, consumed := range document.API.Consumes { + alias := consumed.As + if alias == "" { + alias = consumed.ID + } + aliases = append(aliases, alias) + } + return aliases +} diff --git a/base/go.mod b/base/go.mod new file mode 100644 index 0000000..79985f8 --- /dev/null +++ b/base/go.mod @@ -0,0 +1,80 @@ +module codefly-base/backend + +go 1.27.3 + +// Pinned by the solution-go agent, so a new solution does not have to work the +// graph out. The runtime pin is a RELEASE, never a pseudo-version: a +// pseudo-version names a commit that may never reach a tag, and a solution +// backend must pin the native projected-token runtime — v0.0.12 or later — +// which is what every consumer's consumed-API gate checks for. +// +// This block is `go mod tidy`-clean as generated, with go.sum beside it, so a +// freshly scaffolded backend builds with the default -mod=readonly and no +// network. Moving any pin is a commit that re-runs the solution's boot, not a +// tidy. + +require ( + github.com/codefly-dev/solution-runtime-go v0.0.16 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + connectrpc.com/connect v1.21.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec // indirect + github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec // indirect + github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af // indirect + github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 // indirect + github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 // indirect + github.com/go-openapi/analysis v1.0.0 // indirect + github.com/go-openapi/errors v0.22.9 // indirect + github.com/go-openapi/jsonpointer v1.0.1 // indirect + github.com/go-openapi/jsonreference v1.0.2 // indirect + github.com/go-openapi/loads v0.25.3 // indirect + github.com/go-openapi/spec v1.0.1 // indirect + github.com/go-openapi/strfmt v0.27.2 // indirect + github.com/go-openapi/swag/conv v0.29.2 // indirect + github.com/go-openapi/swag/jsonutils v0.29.2 // indirect + github.com/go-openapi/swag/loading v0.29.2 // indirect + github.com/go-openapi/swag/mangling v0.29.2 // indirect + github.com/go-openapi/swag/pools v0.29.2 // indirect + github.com/go-openapi/swag/stringutils v0.29.2 // indirect + github.com/go-openapi/swag/typeutils v0.29.2 // indirect + github.com/go-openapi/swag/yamlutils v0.29.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gofrs/flock v0.13.1 // indirect + github.com/google/go-github/v89 v89.0.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modelcontextprotocol/go-sdk v1.8.0 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/base/go.sum b/base/go.sum new file mode 100644 index 0000000..fda2a72 --- /dev/null +++ b/base/go.sum @@ -0,0 +1,271 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs= +connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +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/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= +github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec h1:gjLHn5hsteMDx8TeNj+KPBsa06nLwzAEhyR2IfC3JWs= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= +github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec h1:QhncdaWaMERAGpZthlSXjKroXNl/L8llPtBMX+e5w/A= +github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:NqCbvIWbkbCzSpUUYQjP8Jv+LpFnDRbVhPdAj67DHhE= +github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af h1:JLTecMs4Oja+fl51chLyGw262S4yz7jzEEiFxoq7Viw= +github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= +github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= +github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 h1:hi+KhotTLMOJ/PUSzVO/Axfho7yZ2UOjrCFfrLI1EKU= +github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458/go.mod h1:Qg8SMzZxKSllwvZwShMrWgSetBVMjxP0s17mwWHWjk0= +github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 h1:wBOL7QsuoC0HsS43gsH2+tpsipk4os077C2VUDkMUHg= +github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458/go.mod h1:hwtrdGtgh8i2xzjgihyd4wY2u+UK7nHQz30OGKL0n+M= +github.com/codefly-dev/solution-runtime-go v0.0.16 h1:I6BpEvzw3PsMYopyDfBCVx2Zcf7yFh6uNqCQcz6MxR0= +github.com/codefly-dev/solution-runtime-go v0.0.16/go.mod h1:sfriXiK34I7k5Ad1Y1VxxNdChsSmzmEJEj3s+IwQlfk= +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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= +github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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/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.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v1.0.0 h1:sNvbAGCJqUTqIAodr9IVqJMmuZas3YS9ms1dGK9yiJ4= +github.com/go-openapi/analysis v1.0.0/go.mod h1:NhYjJ57fnE+bcE7UwrJyMkhWA3Dfz7TdiBfTVAnos4Y= +github.com/go-openapi/errors v0.22.9 h1:HI9+SyVYiRzyeBQGv0CbWbQa+u2S0nny7G7AhY+UMiM= +github.com/go-openapi/errors v0.22.9/go.mod h1:R73q66sXP2smzAGgFOI4nqtusRK2xN5pYCjsij+KX58= +github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg= +github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY= +github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc= +github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs= +github.com/go-openapi/loads v0.25.3 h1:V+jKy/thXWdLJUuYC8sZX2dICyAa8M3DokF70jj34P0= +github.com/go-openapi/loads v0.25.3/go.mod h1:LgLyCSOLBL2Qnj0Ps1oo2YH8jZoEKsMeVZCc+ALIxFo= +github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk= +github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8= +github.com/go-openapi/strfmt v0.27.2 h1:SG32SlbwNy92s0KJiVxt2joJeFdqIYHvwrA0OU6HqzQ= +github.com/go-openapi/strfmt v0.27.2/go.mod h1:M4CKsMO0Fb8qR10+1Ra75wCKNNquy+Vj+4LWZrhTo2E= +github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= +github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= +github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= +github.com/go-openapi/swag/cmdutils v0.29.2/go.mod h1:XziaSVzYqkDvpWXzASfWajw2YvJrLscq610O2XiyqJw= +github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0= +github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q= +github.com/go-openapi/swag/fileutils v0.29.2 h1:mdUL+Vw5ah1fO1AvFCoHeIyv7YZsCz2MN3KNqd/lLVI= +github.com/go-openapi/swag/fileutils v0.29.2/go.mod h1:7QKmmodjAebaq61lGVJa8ldLdGeMF4DSgYv8tXOGGo0= +github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k= +github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM= +github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY= +github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI= +github.com/go-openapi/swag/mangling v0.29.2 h1:ltdo3K0tTP4P6zWhVVWDU9D37/YYBuMJRdZxiS2Dtog= +github.com/go-openapi/swag/mangling v0.29.2/go.mod h1:RjDi4TnItAenS3cgCXSpIl1kKbjG7il6pnmQVl/8bp8= +github.com/go-openapi/swag/netutils v0.29.2 h1:dBli6jyUa93sDS/XYWShJzf4+nnVfFKfvY9SLUaUzB8= +github.com/go-openapi/swag/netutils v0.29.2/go.mod h1:1pM6Xg/Um8E1eps3umWl8Hme3ByWXOtHsvFC0CULHUE= +github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c= +github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc= +github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk= +github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI= +github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I= +github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE= +github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM= +github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= +github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho= +github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo= +github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE= +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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +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-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0= +github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho= +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/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +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/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-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/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/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +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/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= +github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +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/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4= +github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74= +github.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +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/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 h1:Tq+E65HaNbZTeqcLw6QUhsV+/EIWdkfUH2CQzcrlE5M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0/go.mod h1:A+EVhDakAj1waJBPhmTszZaSr08ww8MRRKy1nB1rqck= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +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.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +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.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +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.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b h1:nB3spRIaSPtxJpxb0k41yljROvm2Jrwfs2pSNdKS5y8= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b/go.mod h1:9N4vtjEwvgQKEdlFVQO0M44ErhecQ9DCSQfGoZ51M6w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 h1:QG6hS4zFplRZvOQedzei4t2pT/zkQc3VR1u+S9/rcAw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/base/main.go b/base/main.go new file mode 100644 index 0000000..a0f5063 --- /dev/null +++ b/base/main.go @@ -0,0 +1,63 @@ +// Command backend is the server for the "Codefly Base" solution. +// +// Everything a solution needs identically is the solution runtime's: it +// presents what this solution is, answers the health probe the endpoint +// declares, serves the page's calls, and relays the page's module calls to +// the authority's hop AS THE VIEWER, where each module authorizes the call. +// +// What is left here is this solution's own composition: what it is (below), +// which routes it serves itself (operations.go), and which module operations +// its page may call (consumes.go). +package main + +import ( + _ "embed" + "log" + + solution "github.com/codefly-dev/solution-runtime-go" +) + +// The runtime and the CLI read ONE declared frontend block: the runtime serves +// it at /.well-known/solution.json and the CLI delivers it in the presence +// document it signs. Keeping it beside this service — and embedding it rather +// than restating it — is what keeps the served manifest and the signed +// presence from becoming two files that disagree. +// +//go:embed solution.frontend.codefly.yaml +var frontendDeclaration []byte + +// solutionID is this solution's ONE name: the id the host's presence document +// names it by, the alias the authority's edge routes it at (/v1/codefly-base), +// the principal the authority issues to, and the Codefly module name. +const solutionID = "codefly-base" + +// manifest is what this solution is. +// +// Capabilities are the feature ids the host's capability handshake reports +// (/.well-known/capabilities). Name one per thing this solution can actually +// do — "documents.read" once its page reads the document store, "read-only" +// when it writes nothing anywhere — and nothing it cannot: a host lists a +// solution by what it claims here. +var manifest = solution.Manifest{ + ID: solutionID, + Contract: solutionID, + Frontend: frontendDeclaration, +} + +func main() { + // A composition that names no authority is refused at boot, by name: + // there is no other way from here to a module. + if err := server().Serve(); err != nil { + log.Fatal(err) + } +} + +// server is this solution on the authority interface: the routes it serves +// itself, and the modules it consumes relayed by the runtime's passthrough. +// +// ONE declaration both serves the solution and renders its interface document +// (openapi.go), so a route cannot be served without publishing the policy a +// call to it must hold, nor published without being served. +func server() *solution.Server { + return solution.New(manifest).Consumes(consumed()...).Operations(operations()...) +} diff --git a/base/openapi.go b/base/openapi.go new file mode 100644 index 0000000..fc92475 --- /dev/null +++ b/base/openapi.go @@ -0,0 +1,28 @@ +package main + +import solution "github.com/codefly-dev/solution-runtime-go" + +//go:generate go test -run TestInterfaceArtifactIsCurrent -update . + +// interfaceArtifactPath is this backend's interface document: every route it +// serves under the authority interface, each with the policy a call to it must +// hold, rendered by the solution runtime from the server itself at the version +// service.codefly.yaml declares. +// +// It is the document `codefly generate contracts` publishes as the http +// endpoint's contract, and the CLI derives the route rows the authority's +// Check applies from it — so a backend without one publishes no contract, and +// a route served without being in it is a route the authority has no row for. +// `go generate ./...` rewrites it and openapi_test.go fails on a diff. +const interfaceArtifactPath = "openapi/api.swagger.json" + +// renderInterfaceArtifact renders the document for the backend rooted at dir. +func renderInterfaceArtifact(dir string) ([]byte, error) { + return server().InterfaceArtifact(dir, solution.InterfaceInfo{ + Title: "codefly-base solution backend", + Description: "The codefly-base solution's backend under the authority interface, at the route the authority's edge forwards it on: the routes it serves itself, and the module calls the solution runtime relays as the viewer. A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, and the module's own method policy, gate it.", + // No base path: every route already carries /v1/codefly-base, as served. + OperationPrefix: "CodeflyBase", + Tag: solutionID, + }) +} diff --git a/base/openapi/api.swagger.json b/base/openapi/api.swagger.json new file mode 100644 index 0000000..cea613b --- /dev/null +++ b/base/openapi/api.swagger.json @@ -0,0 +1,39 @@ +{ + "consumes": [ + "application/json" + ], + "definitions": { + "Error": { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + } + }, + "type": "object" + } + }, + "info": { + "description": "The codefly-base solution's backend under the authority interface, at the route the authority's edge forwards it on: the routes it serves itself, and the module calls the solution runtime relays as the viewer. A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, and the module's own method policy, gate it.", + "title": "codefly-base solution backend", + "version": "0.0.0" + }, + "paths": {}, + "produces": [ + "application/json" + ], + "security": [ + { + "workContext": [] + } + ], + "securityDefinitions": { + "workContext": { + "description": "The signed-in person's Work Context for this solution, sent by the authority's edge. Every operation requires it.", + "in": "header", + "name": "x-codefly-work-context", + "type": "apiKey" + } + }, + "swagger": "2.0" +} diff --git a/base/openapi_test.go b/base/openapi_test.go new file mode 100644 index 0000000..6213af2 --- /dev/null +++ b/base/openapi_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "flag" + "os" + "path/filepath" + "testing" +) + +var update = flag.Bool("update", false, "rewrite "+interfaceArtifactPath+" from the routes this backend serves") + +// TestInterfaceArtifactIsCurrent holds the committed interface document to the +// routes this backend actually serves. It is rendered from the server itself, +// so a route added without re-rendering fails here rather than reaching the +// authority with no row to check it against. +// +// `go generate ./...` runs this with -update. +func TestInterfaceArtifactIsCurrent(t *testing.T) { + rendered, err := renderInterfaceArtifact(".") + if err != nil { + t.Fatalf("cannot render %s: %v", interfaceArtifactPath, err) + } + path := filepath.FromSlash(interfaceArtifactPath) + if *update { + if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, rendered, 0o600); err != nil { + t.Fatal(err) + } + return + } + committed, err := os.ReadFile(path) + if err != nil { + t.Fatalf("cannot read %s: %v\nRun `go generate ./...` and commit it.", interfaceArtifactPath, err) + } + if !bytes.Equal(bytes.TrimSpace(committed), bytes.TrimSpace(rendered)) { + t.Fatalf("%s is not what this backend serves. Run `go generate ./...` and commit it.", interfaceArtifactPath) + } +} diff --git a/base/operations.go b/base/operations.go new file mode 100644 index 0000000..123a5dd --- /dev/null +++ b/base/operations.go @@ -0,0 +1,40 @@ +package main + +import solution "github.com/codefly-dev/solution-runtime-go" + +// route is where the authority's edge routes this solution and forwards its +// requests unchanged: /v1/, the solution's alias. EVERY operation carries +// it, because the runtime asks the authority about the route as served — a +// path declared without it is a row no request matches. +const route = "/v1/" + solutionID + +// operations is what this backend serves itself, beside the runtime's +// passthrough. +// +// A freshly scaffolded solution serves none of its own: the host lists it, +// mounts its page, and the page reads what it reads through the modules this +// solution consumes (consumes.go). Add one per route this solution answers +// with work that is its own. +// +// Policy is REQUIRED on every operation. The authority applies it to each +// call, and an absent declaration and "nothing in particular is required" +// must not look the same — so a route that needs nothing beyond a checked +// caller says so with solution.AnyCheckedCaller(): +// +// solution.Operation{ +// Path: route + "/notes", Method: http.MethodGet, +// Summary: "list the notes the viewer may read", +// Callers: "The page's Notes view.", +// Behavior: "Answers the viewer's own notes, newest first, 50 at a time.", +// Response: notesResponse{}, +// Policy: solution.Requires("codefly-base.note", "read"), +// Handler: listNotes, +// } +// +// The resource kind a policy names is this solution's OWN vocabulary, which +// module.contract.codefly.yaml declares under scope_ceilings. A kind the +// contract does not declare is a scope the authority never issues, so the +// route would refuse every call. +func operations() []solution.Operation { + return nil +} diff --git a/base/service.codefly.yaml b/base/service.codefly.yaml new file mode 100644 index 0000000..a700cd4 --- /dev/null +++ b/base/service.codefly.yaml @@ -0,0 +1,62 @@ +# The backend's manifest, as it stands once `codefly add solution` has run. +# +# This agent's Create writes NONE of the solution shape below: the CLI +# (cli#1216, applyEntryServiceShape) states module-identity, the endpoints and +# the service-dependencies on this manifest after Create returns. The file is +# here so base/ is a real service whose own suite can run — consumes_test.go +# reads it to refuse a module edge — and it is excluded from generation so no +# scaffolded tree ever carries a second copy of the CLI's declarations. +# +# It is kept identical to what the CLI writes +# (cmd/add/solution.go solutionEntryDependencies and solutionEntryEndpoint); +# base_test.go fails if the two drift. +kind: service +name: backend +version: 0.0.0 +agent: + publisher: codefly.dev + kind: codefly:service + name: solution-go + version: 0.0.1 +spec: + source-dir: . +# The backend presents the solution's identity: the authority takes its +# projected workload credential as the actor of every Work Context it issues +# the solution, so the authority document the CLI renders from the module +# contract approves this service's build. +module-identity: true +service-dependencies: + # The authority interface, required by `interface:` alone so the workspace + # binds it to the one authority in scope. check_solution_mesh reads an + # interface-only entry as the host's door. + - interface: codefly.dev/authority@>=0.2.0 <0.4.0 + # The authority's hop: the one address the runtime's passthrough dials, which + # lets through only a call the authority checked. `external` — composed by + # the workspace that composes this solution, never started from here — so it + # consumes no endpoint and derives no mesh allow rule: it names the edge and + # opens none. + - name: hop + module: authority + kind: external + endpoints: + - name: grpc +workspace-configuration-dependencies: + # The authority interface's address, and this solution's own slots: the + # _ENDPOINT and _BINDING each consumed module is reached through + # (consumes.go). + - authority + - codefly-base +endpoints: + # The one endpoint a solution backend serves. `exposure: none` makes it + # reachable inside the cell and addressed from nowhere outside it — what the + # outside reaches is the host's own door. + - name: http + visibility: public + exposure: none + api: http + secured: false + health: + readiness: + kind: http + path: /health + statuses: ["200"] diff --git a/base/service.generation.codefly.yaml b/base/service.generation.codefly.yaml new file mode 100644 index 0000000..a200387 --- /dev/null +++ b/base/service.generation.codefly.yaml @@ -0,0 +1,79 @@ +# HOW base/ BECOMES templates/factory/. +# +# base/ is a REAL solution backend: it compiles, it has a go.sum, and its own +# test suite is the suite every scaffolded solution ships with. templates/ +# factory/ is GENERATED from it by core's generator.GenerateServiceTemplate +# (`codefly agent generate --service .`), which copies every file here, adds a +# .tmpl suffix, and applies the literal replacements below. +# +# Nothing in templates/factory is hand-edited. factory_test.go regenerates into +# a temporary directory and fails on any difference, and `go test -run +# TestFactoryTemplatesAreGeneratedFromBase -update` is what moves it. +# +# The replacements are literal, whole-file string substitutions applied IN +# ORDER (core's templates.ServiceReplacer), so each `from` must be a token that +# appears nowhere else and no `from` may be a substring of an earlier `to`. +# `codefly-base` is the base solution's name and is spelled four ways, three of +# which are derived from it exactly as scaffold.go derives them for a real +# solution: +# +# codefly-base the solution id -> SolutionID (scaffold.go: Module.Name.LowerCase) +# Codefly Base the placeholder title -> Title (titleFrom) +# CodeflyBase the operation prefix -> OperationPrefix (aliasField) +# +# The fourth, `codefly_base`, is the JavaScript identifier and appears only in +# templates/page, which is not generated from here (it is an npm package, not a +# Go service, so GenerateServiceTemplate does not produce it). +# +# `backend` is NOT replaced, and that is deliberate. A solution's entry service +# is named `backend` by the shape's own definition — the CLI's +# solutionEntryService and this agent's backendService are both that constant — +# so it is not a parameter. It also appears in prose on nearly every line here +# ("a solution backend", "the compiled backend"), which a whole-file +# substitution would corrupt. +replacements: + # Longest and most specific first: `CodeflyBase` would otherwise never match + # once `Codefly Base` had been rewritten, and neither contains the other. + - from: CodeflyBase + to: "{{ .OperationPrefix }}" + - from: Codefly Base + to: "{{ .Title }}" + - from: codefly-base + to: "{{ .SolutionID }}" +ignores: + # This file. + - service.generation.codefly.yaml + # THE THREE DOCUMENTS THE CLI OWNS. They are here so base/ is a real service + # that compiles and passes its own tests; they are never templatized, because + # `codefly add solution` (cli#1216) writes them AFTER this agent's Create + # returns and anything written here would be overwritten silently by landing + # first. shape_test.go holds the generated tree to that. + # + # service.codefly.yaml carries module-identity, the endpoints and the only + # two service-dependencies a solution backend has. core's own generator has a + # hardcoded check for this name, but it compares a FULL path and so never + # fires; the exclusion that works is this line. + - service.codefly.yaml + # The frontend block main.go embeds. The runtime serves it and the CLI + # delivers it in the presence document it signs, so one file is the only way + # the served manifest and the signed presence cannot disagree. + - solution.frontend.codefly.yaml + # The interface document. The agent renders the first copy with the solution + # runtime's OWN renderer at Create (scaffold.go writeInterfaceArtifact), so + # the committed bytes are the bytes the scaffolded `go generate ./...` + # writes. A template here would be a second mechanism for one file, and the + # template's bytes are not the renderer's. Spelled as a path, so it excludes + # the document without touching openapi.go or openapi_test.go — the ignore is + # a substring match. + - openapi/api.swagger.json + # The one agent-OWNED file in a scaffolded backend. consumed_gen.go is + # rendered from the solution manifest's api.consumes by this agent + # (consumption.go renderConsumption) at Create and rewritten by `codefly + # sync`; a template would be a second mechanism, and its bytes would be + # base's consumption rather than the new solution's. It is here so base/ + # compiles, and base_test.go holds it to what renderConsumption produces for + # base's own manifest. + - consumed_gen.go + # A workspace is a local convenience and never part of a scaffolded tree. + - go.work + - go.work.sum diff --git a/base/solution.frontend.codefly.yaml b/base/solution.frontend.codefly.yaml new file mode 100644 index 0000000..4105d71 --- /dev/null +++ b/base/solution.frontend.codefly.yaml @@ -0,0 +1,8 @@ +# The page declaration, as `codefly add solution --title` writes it. It is here +# so base/ compiles — main.go embeds it, and //go:embed cannot reach a file +# that does not exist — and it is excluded from generation, because the CLI +# writes this file itself after Create returns. +title: Codefly Base +order: 60 +exposed_module: ./Page +host_contract: 1 diff --git a/base_test.go b/base_test.go new file mode 100644 index 0000000..76b5272 --- /dev/null +++ b/base_test.go @@ -0,0 +1,255 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + solution "github.com/codefly-dev/solution-runtime-go" + "github.com/stretchr/testify/require" +) + +// base/ IS THE HAND-WRITTEN ARTIFACT, and templates/factory/ is generated +// from it (factory_test.go). That is the whole authoring model of a service +// agent, and the reason for it is this file: a template cannot be compiled or +// tested, so nothing proves what it scaffolds even builds. base/ is a real +// solution backend — it compiles, it has a tidy go.sum, and ITS OWN TEST SUITE +// IS THE SUITE EVERY SCAFFOLDED SOLUTION SHIPS WITH. Running it here is what +// makes the scaffolded tree known-good before anybody scaffolds it. +// +// `go test -update` rewrites the three derived files in base/ and regenerates +// templates/factory. Without it, every one of them is checked. + +// update rewrites what is derived rather than asserting it. One flag for this +// file and factory_test.go, so `-update` moves base/ and the templates +// together and they cannot be refreshed one without the other. +var update = flag.Bool("update", false, + "rewrite base/'s derived files and regenerate templates/factory from base/") + +const ( + // baseDir is the real service this agent is authored as. + baseDir = "base" + // baseSolutionID is base/'s solution name, and the token the generation + // config replaces to produce the templates. Spelled here so a test reads + // the same name the config does. + baseSolutionID = "codefly-base" +) + +// baseSolutionManifest is the solution manifest base/ belongs to: the document +// `codefly add solution --consumes` writes one directory above services/. +// +// It is NOT in base/ — base/ is the service directory, so the manifest sits at +// ../../ from it, outside this repository. stageBase puts it where a real +// solution has it. +// +// It declares one consumed module ON PURPOSE. A solution that consumes +// something is the state that was broken before: the runtime refuses a +// consumed module declaring no method, so a backend whose composition is still +// empty must leave an uncomposed module OUT rather than declare it empty. +// base/ is therefore permanently in that state, and its suite would fail if +// consumes.go stopped filtering. +const baseSolutionManifest = `schema_version: codefly.solution-manifest/v1 +protocol_version: codefly.solution/v0 +api: + exposes: + - id: http + protocol: http + consumes: + - id: documents + protocol: connect + module: documents + service: documents + endpoint: grpc + as: documents +lifecycle: + package: true + render: true +` + +// stageBase lays base/ out the way a real solution lays its backend out — +// /solution.codefly.yaml beside /services/backend — and returns +// the backend directory. +// +// base/ cannot be tested where it sits: consumes_test.go reads the solution +// manifest at ../../solution.codefly.yaml, which from /base is outside +// the repository. Staging is not a workaround for that, it is the only honest +// way to run the suite, because ../../ is where a scaffolded backend really +// finds its manifest and a test that reached for it anywhere else would not be +// checking the shipped path. +func stageBase(t *testing.T) string { + t.Helper() + root := t.TempDir() + backend := filepath.Join(root, "services", backendService) + require.NoError(t, os.MkdirAll(filepath.Dir(backend), 0o750)) + require.NoError(t, os.CopyFS(backend, os.DirFS(baseDir))) + require.NoError(t, os.WriteFile(filepath.Join(root, "solution.codefly.yaml"), + []byte(baseSolutionManifest), 0o600)) + return backend +} + +// TestBaseCompilesAndPassesItsOwnSuite is the check a hand-written template +// could never carry. +// +// `go build` runs with the default -mod=readonly: base/'s go.mod and go.sum are +// tidy as committed, so a scaffolded backend builds without a tidy. `go vet` +// runs because a scaffolded solution is code somebody will read and extend. +// `go test` runs base/'s own suite — the consumption drift check, the refusal +// of a module edge (SP-SOL-04) and the interface document — which is the suite +// every scaffolded solution gets. +func TestBaseCompilesAndPassesItsOwnSuite(t *testing.T) { + goTool := goToolchain(t) + backend := stageBase(t) + for _, command := range [][]string{ + {"build", "./..."}, + {"vet", "./..."}, + {"test", "./..."}, + } { + run(t, goTool, backend, command...) + } +} + +// TestBaseConsumptionIsWhatThisAgentGenerates. consumed_gen.go is the one +// agent-OWNED file in a scaffolded backend: rendered from api.consumes by +// renderConsumption, never templatized. base/ commits a copy so it compiles, +// and this holds that copy to the generator — so base/ cannot drift into +// compiling against a shape the agent no longer produces. +func TestBaseConsumptionIsWhatThisAgentGenerates(t *testing.T) { + aliases, err := consumedAliases(stageBase(t)) + require.NoError(t, err) + require.Equal(t, []string{"documents"}, aliases, + "base/'s manifest declares the one consumed module its suite checks") + + path := filepath.Join(baseDir, generatedConsumptionFile) + rendered := renderConsumption(aliases) + if *update { + require.NoError(t, os.WriteFile(path, rendered, 0o600)) + return + } + committed, err := os.ReadFile(path) //nolint:gosec // a path in this repository + require.NoErrorf(t, err, "base/%s is missing; run `go test -update`", generatedConsumptionFile) + require.Equalf(t, string(rendered), strings.ReplaceAll(string(committed), "\r\n", "\n"), + "base/%s is not what renderConsumption produces; run `go test -update`", generatedConsumptionFile) +} + +// TestBaseInterfaceDocumentIsTheRuntimesRender. The agent renders a new +// solution's first interface document with the solution runtime's own +// renderer, so the committed bytes are the bytes the scaffolded `go generate +// ./...` writes. base/ commits the same document for its own openapi_test.go, +// and it must come from the same render — otherwise base/'s suite passes on +// bytes a scaffolded tree would never hold. +func TestBaseInterfaceDocumentIsTheRuntimesRender(t *testing.T) { + server := solution.New(solution.Manifest{ID: baseSolutionID, Contract: baseSolutionID}) + document, err := server.InterfaceArtifact("", solution.InterfaceInfo{ + Title: baseSolutionID + " solution backend", + Version: "0.0.0", + Description: interfaceDescription(baseSolutionID), + OperationPrefix: aliasField(baseSolutionID), + Tag: baseSolutionID, + }) + require.NoError(t, err) + + path := filepath.Join(baseDir, filepath.FromSlash(interfaceArtifactPath)) + if *update { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o750)) + require.NoError(t, os.WriteFile(path, document, 0o600)) + return + } + committed, err := os.ReadFile(path) //nolint:gosec // a path in this repository + require.NoErrorf(t, err, "base/%s is missing; run `go test -update`", interfaceArtifactPath) + require.Equalf(t, strings.TrimSpace(string(document)), + strings.TrimSpace(strings.ReplaceAll(string(committed), "\r\n", "\n")), + "base/%s is not the runtime's render; run `go test -update`", interfaceArtifactPath) +} + +// TestBasePinsTheRuntimeThisAgentDeclares. The pin is the AGENT's choice, one +// value in every solution it scaffolds, so it is a literal in base/go.mod +// rather than a template action — and it must be the constant this agent +// declares. It must also clear the floor a consumer's consumed-API gate +// enforces: a release at or after v0.0.12, the native projected-token runtime. +// A pseudo-version would pin a commit that may never reach a tag. +func TestBasePinsTheRuntimeThisAgentDeclares(t *testing.T) { + modules := read(t, filepath.Join(baseDir, "go.mod")) + require.Containsf(t, modules, solutionRuntimePath+" "+solutionRuntimeVersion, + "base/go.mod must pin %s at %s, the version scaffold.go declares", + solutionRuntimePath, solutionRuntimeVersion) + require.NotContains(t, modules, solutionRuntimePath+" =>", + "a replace makes the pin invisible to the consumed-API gate") + require.Regexp(t, `^v\d+\.\d+\.\d+$`, solutionRuntimeVersion, + "the runtime pin is a release, never a pseudo-version") + require.GreaterOrEqual(t, solutionRuntimeVersion, "v0.0.12", + "the pin must clear the native projected-token floor") +} + +// TestBaseManifestIsTheHostsDoorOnly. base/service.codefly.yaml is excluded +// from generation — the CLI writes it — but base/'s own suite reads it to +// refuse a module edge, so it must be what the CLI actually writes. If it +// drifted to something laxer, base's refusal would be checked against a +// manifest no solution has. +func TestBaseManifestIsTheHostsDoorOnly(t *testing.T) { + assertHostsDoorOnly(t, read(t, filepath.Join(baseDir, "service.codefly.yaml"))) +} + +// TestBaseHoldsEveryFileItsGenerationConfigExcludes, and no others. +// +// An ignore is only correct while the file it names is present: an ignore for +// a file nobody committed silently protects nothing, and a file that should be +// excluded but is not becomes a template that overwrites one of the CLI's +// documents. Both directions are checked here. +func TestBaseHoldsEveryFileItsGenerationConfigExcludes(t *testing.T) { + configuration := baseGeneration(t) + // Every ignore that names a real path must name a file base/ holds. The + // workspace entries are the exception: a go.work is a local convenience + // that is deliberately NOT committed, and the ignore exists so one in a + // working tree never reaches the templates. + local := map[string]bool{"go.work": true, "go.work.sum": true} + for _, ignore := range configuration.Ignores { + if local[ignore] { + require.NoFileExists(t, filepath.Join(baseDir, ignore), "a workspace is never committed") + continue + } + require.FileExistsf(t, filepath.Join(baseDir, filepath.FromSlash(ignore)), + "the generation config ignores %q, which base/ does not hold: the ignore protects nothing", ignore) + } + // The documents the CLI owns must each be excluded. This is the direction + // that matters: one of them templatized would land in a scaffolded tree + // and be overwritten by the CLI a moment later. + for _, owned := range []string{ + "service.codefly.yaml", + "solution.frontend.codefly.yaml", + interfaceArtifactPath, + generatedConsumptionFile, + } { + require.Containsf(t, configuration.Ignores, owned, + "base/%s is not excluded from generation, so it would be scaffolded as a template", owned) + } +} + +// goToolchain is the `go` to build base/ with, or a skip. This package is a Go +// program, so a tree that can run it has one; the skip is for a sandbox that +// has stripped PATH. +func goToolchain(t *testing.T) string { + t.Helper() + tool, err := exec.LookPath("go") + if err != nil { + t.Skip("no go toolchain on PATH to build base/ with") + } + return tool +} + +// run is one go command in a staged tree, with its full output on failure: +// what failed to compile is the finding, not the exit status. +// +// GOWORK=off keeps the staged module out of any go.work this checkout sits in +// — it is a separate module in a temporary directory, and a workspace would +// try to resolve it against this repository's. +func run(t *testing.T, goTool, dir string, args ...string) { + t.Helper() + command := exec.Command(goTool, args...) + command.Dir = dir + command.Env = append(os.Environ(), "GOWORK=off") + output, err := command.CombinedOutput() + require.NoErrorf(t, err, "`go %s` in the staged base/:\n%s", strings.Join(args, " "), output) +} diff --git a/endtoend_test.go b/endtoend_test.go index a9dc0c4..7d909c5 100644 --- a/endtoend_test.go +++ b/endtoend_test.go @@ -2,9 +2,7 @@ package main import ( "os" - "os/exec" "path/filepath" - "strings" "testing" "github.com/stretchr/testify/require" @@ -12,12 +10,14 @@ import ( // THE VERIFICATION BAR, run rather than asserted. // -// Every other test in this package reads a template. This one scaffolds a -// solution the way `codefly add solution` scaffolds one, completes it the way -// the CLI completes it, and then COMPILES it and runs the suite it ships -// with. A template that renders into Go that does not build, or into a tree -// whose own gates are red the moment it lands, passes every other test here -// and fails this one. +// base_test.go compiles base/, which is the hand-written source. This compiles +// what a USER ends up with, which is a different thing and can fail while +// base/ is green: it goes through this agent's own Create — the generated +// templates rendered with the scaffold context, plus the two files the agent +// writes itself (consumed_gen.go and the interface document) — and then +// completes the tree the way the CLI completes it. A replacement that renders +// into Go that does not compile, or an agent-written file that disagrees with +// what the templates expect, passes every other test here and fails this one. // // It is what caught both defects this file was added with: a solution that // consumed anything was born red, because the runtime refuses a consumed @@ -74,39 +74,11 @@ func completeAsTheCLIDoes(t *testing.T, backend string) { // What `codefly add service` writes before the solution's own block: the // identity and the source convention. `spec.source-dir: .` is this agent's // (service.go says why the module is at the service root). - base := "kind: service\nname: " + backendService + "\nversion: 0.0.0\n" + + header := "kind: service\nname: " + backendService + "\nversion: 0.0.0\n" + "agent:\n publisher: " + agent.Publisher + "\n kind: codefly:service\n" + " name: " + agent.Name + "\n version: " + agent.Version + "\n" + "spec:\n source-dir: " + solutionSourceDir + "\n" declared := fencedYAML(t, read(t, filepath.Join(backend, "GETTING_STARTED.md"))) require.NoError(t, os.WriteFile(filepath.Join(backend, "service.codefly.yaml"), - []byte(base+declared+"\n"), 0o600)) -} - -// goToolchain is the `go` to build the scaffolded module with, or a skip. The -// agent itself is a Go program, so a tree that can run this package has one; -// the skip is for a sandbox that has stripped PATH. -func goToolchain(t *testing.T) string { - t.Helper() - tool, err := exec.LookPath("go") - if err != nil { - t.Skip("no go toolchain on PATH to build the scaffolded solution with") - } - return tool -} - -// run is one go command in the scaffolded backend, with its full output on -// failure: what failed to compile is the finding, not the exit status. -// -// GOWORK=off keeps the scaffolded module out of any go.work the agent's own -// checkout sits in — it is a separate module in a temporary directory, and a -// workspace would try to resolve it against this repository's. -func run(t *testing.T, goTool, dir string, args ...string) { - t.Helper() - command := exec.Command(goTool, args...) - command.Dir = dir - command.Env = append(os.Environ(), "GOWORK=off") - output, err := command.CombinedOutput() - require.NoErrorf(t, err, "`go %s` in the scaffolded backend:\n%s", - strings.Join(args, " "), output) + []byte(header+declared+"\n"), 0o600)) } diff --git a/factory_test.go b/factory_test.go new file mode 100644 index 0000000..32bed9e --- /dev/null +++ b/factory_test.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/codefly-dev/core/agents/generator" + "github.com/codefly-dev/core/generation" + "github.com/codefly-dev/core/resources" + "github.com/stretchr/testify/require" +) + +// TEMPLATES/FACTORY IS GENERATED. NOTHING IN IT IS HAND-EDITED. +// +// `codefly agent generate --service .` is core's +// generator.GenerateServiceTemplate: it copies base/ into templates/factory, +// adds a .tmpl suffix to every name, and applies the literal replacements in +// base/service.generation.codefly.yaml. +// +// This test is that generator, run into a temporary directory and compared +// against what is committed — the drift check. It calls core's function +// directly rather than shelling out to the CLI, so the check needs no second +// binary and holds the committed templates to the exact code `codefly agent +// generate` runs. +// +// `go test -run TestFactoryTemplatesAreGeneratedFromBase -update` regenerates +// them in place. An edit made to a template instead of to base/ is reverted by +// the next regeneration, and fails here until it is. + +// TestFactoryTemplatesAreGeneratedFromBase. +func TestFactoryTemplatesAreGeneratedFromBase(t *testing.T) { + generated := generateFactory(t) + committed := filepath.Join("templates", "factory") + + if *update { + require.NoError(t, os.RemoveAll(committed), + "the committed templates are replaced wholesale: regeneration overwrites a file but never deletes one, so a template whose source left base/ would survive") + require.NoError(t, os.MkdirAll(filepath.Dir(committed), 0o750)) + require.NoError(t, os.CopyFS(committed, os.DirFS(generated))) + return + } + + want, got := treeOf(t, generated), treeOf(t, committed) + + // The file sets first, and both directions. Regeneration overwrites but + // never deletes, so a template left behind by a file that has since been + // removed from base/ would otherwise go on being scaffolded forever. + require.Equal(t, keysOf(want), keysOf(got), + "templates/factory is not the file set base/ generates — run `go test -run TestFactoryTemplatesAreGeneratedFromBase -update`") + + for _, name := range keysOf(want) { + require.Equalf(t, want[name], got[name], + "templates/factory/%s is not what base/ generates. It is GENERATED: edit base/ and run `go test -run TestFactoryTemplatesAreGeneratedFromBase -update`.", name) + } +} + +// TestGeneratedTemplatesCarryNoBaseSolutionName is the check that the +// replacements are complete. +// +// base/ is a real solution called `codefly-base`. Every mention of that name, +// in any of its three spellings, must have become a template action — a +// leftover would hard-code base/'s own name into every solution this agent +// scaffolds. The replacements are whole-file literal substitutions, so a new +// spelling introduced in base/ (a different capitalisation, say) silently +// survives generation; this is what catches it. +func TestGeneratedTemplatesCarryNoBaseSolutionName(t *testing.T) { + for name, content := range treeOf(t, filepath.Join("templates", "factory")) { + for _, spelling := range []string{baseSolutionID, "Codefly Base", "CodeflyBase", "codefly_base"} { + require.NotContainsf(t, content, spelling, + "templates/factory/%s still carries base/'s own solution name %q: add a replacement to base/service.generation.codefly.yaml", name, spelling) + } + } +} + +// TestGeneratedTemplatesRenderWithTheScaffoldContext. A replacement whose `to` +// names a field the scaffold context does not carry produces a template that +// fails at Create, in a user's terminal. Every action in the generated tree +// must be one this agent can fill, which the scaffold tests below prove by +// rendering the whole tree; this states the narrower rule the replacements +// must obey. +func TestGeneratedTemplatesRenderWithTheScaffoldContext(t *testing.T) { + known := map[string]bool{ + "{{ .SolutionID }}": true, "{{ .Title }}": true, "{{ .OperationPrefix }}": true, + } + for _, replacement := range baseGeneration(t).Replacements { + require.Containsf(t, known, replacement.To, + "base/service.generation.codefly.yaml replaces %q with %q, which the scaffold context does not carry", + replacement.From, replacement.To) + } +} + +// generateFactory runs core's generator over a copy of base/ and returns the +// directory holding the generated templates/factory. +// +// base/ is copied rather than generated from in place, because +// GenerateServiceTemplate writes to /templates/factory and this must not +// touch the committed tree while comparing against it. +func generateFactory(t *testing.T) string { + t.Helper() + root := t.TempDir() + require.NoError(t, os.CopyFS(filepath.Join(root, baseDir), os.DirFS(baseDir))) + require.NoError(t, generator.GenerateServiceTemplate(context.Background(), root), + "`codefly agent generate --service .` failed over base/") + return filepath.Join(root, "templates", "factory") +} + +// baseGeneration is base/'s generation configuration, read with core's own +// loader — the same one GenerateServiceTemplate reads it with. +func baseGeneration(t *testing.T) *generation.Service { + t.Helper() + configuration, err := resources.LoadFromDir[generation.Service](context.Background(), baseDir) + require.NoError(t, err, "base/%s is not a readable generation configuration", + generation.ServiceGenerationConfigurationName) + return configuration +} + +// treeOf is every file under root, by slash-separated path relative to it, +// with line endings normalised so the comparison is of content. +func treeOf(t *testing.T, root string) map[string]string { + t.Helper() + files := map[string]string{} + require.NoError(t, filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil || entry.IsDir() { + return err + } + content, readErr := os.ReadFile(path) //nolint:gosec // a path just walked + if readErr != nil { + return readErr + } + rel, relErr := filepath.Rel(root, path) + if relErr != nil { + return relErr + } + files[filepath.ToSlash(rel)] = strings.ReplaceAll(string(content), "\r\n", "\n") + return nil + }), "cannot read the template tree at %s", root) + require.NotEmpty(t, files, "%s holds no files", root) + return files +} + +func keysOf(files map[string]string) []string { + names := make([]string, 0, len(files)) + for name := range files { + names = append(names, name) + } + sort.Strings(names) + return names +} diff --git a/main.go b/main.go index ce12cc6..30fcf71 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,11 @@ var readmeFS embed.FS // factoryFS is the solution backend: what Create writes into the service // directory. // +// IT IS GENERATED FROM base/, never hand-edited (AGENTS.md). base/ is a real +// solution backend that compiles and whose own suite is the suite every +// scaffolded solution ships with, and `go test -update` regenerates this tree +// from it. factory_test.go fails on any difference. +// //go:embed all:templates/factory var factoryFS embed.FS @@ -86,6 +91,21 @@ var factoryFS embed.FS // which is why a solution's page has been hand-copied out of a sibling every // time (codefly-dev/cli#1217). // +// THIS TREE IS HAND-WRITTEN, unlike templates/factory. Two reasons, and the +// first is structural: core's generator.GenerateServiceTemplate produces +// templates/factory from base/ and nothing else, and this tree is written to a +// SIBLING of the service directory rather than into it, so there is no base/ +// subdirectory it could come from. The second is that the benefit of a base/ — +// that it compiles and is tested — needs `npm install`, and the five +// @codefly-dev kits it depends on are on GitHub Packages behind a +// read:packages token. A base-page/ generated the same way is the right shape +// once CI holds that token and `npm test` would prove something; until then it +// would be a second generator for no verification. +// +// What holds this tree instead: TestThePageSharesEveryHostSingleton pins every +// entry of the Module Federation share policy, which is the part whose failure +// is invisible until a browser loads the remote inside a real host. +// //go:embed all:templates/page var pageFS embed.FS diff --git a/scaffold.go b/scaffold.go index 66f42ff..4dbf640 100644 --- a/scaffold.go +++ b/scaffold.go @@ -79,10 +79,6 @@ type scaffold struct { // identifier fails inside a bundler — the last hand-scaffolded solution's // final friction item. newScaffold refuses it here instead. Identifier string - // RuntimeVersion is the solution runtime the scaffolded go.mod pins, - // stated here so the generated README and GETTING_STARTED name the same - // pin the module requires. - RuntimeVersion string // InterfaceDescription and OperationPrefix are the two values the // interface document carries that are derived rather than typed. They are // in the context, rather than only in the scaffolded openapi.go, because @@ -103,6 +99,10 @@ type scaffold struct { // this constant and to that floor. const solutionRuntimeVersion = "v0.0.16" +// solutionRuntimePath is the module that pin names. It is the module a +// consumer's consumed-API gate looks for in a solution backend's go.mod. +const solutionRuntimePath = "github.com/codefly-dev/solution-runtime-go" + // jsIdentifier is what Module Federation can emit a remote's name as. var jsIdentifier = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`) @@ -125,7 +125,6 @@ func newScaffold(info *services.Information) (scaffold, error) { SolutionID: id, Title: titleFrom(id), Identifier: identifier, - RuntimeVersion: solutionRuntimeVersion, InterfaceDescription: interfaceDescription(id), OperationPrefix: aliasField(id), }, nil diff --git a/shape_test.go b/shape_test.go index ee41029..40cb892 100644 --- a/shape_test.go +++ b/shape_test.go @@ -179,7 +179,26 @@ var mayMentionModuleEdges = map[string]string{ // host's own door: the authority interface required by `interface:` alone, and // `authority/hop` as `kind: external`. func TestTheExampleManifestIsTheHostsDoorOnly(t *testing.T) { - var document struct { + assertHostsDoorOnly(t, fencedYAML(t, templateText(t)["templates/factory/GETTING_STARTED.md.tmpl"])) +} + +// assertHostsDoorOnly reads a solution backend's manifest STRUCTURALLY and +// holds it to the host's own door, as obin-ai/core-solutions' +// check_solution_mesh.py reads a real manifest and for the same reason: four +// spellings of one object defeat a pattern scan, and all four parse to the +// same value. An earlier line-regex here read `- name: grpc` — an endpoint of +// the hop — as a module edge, and would have read a real edge spelled as a +// flow mapping as nothing at all. +// +// Two manifests are held to this, and neither is written by this agent: +// GETTING_STARTED.md's fenced example, which an author copies when the service +// was added with `codefly add service` rather than `codefly add solution`, and +// base/service.codefly.yaml, which base/'s own suite reads to refuse an edge. +// Both must be what the CLI writes, or what they teach and what they check is +// a manifest no solution has. +func assertHostsDoorOnly(t *testing.T, document string) { + t.Helper() + var declared struct { ModuleIdentity bool `yaml:"module-identity"` Dependencies []struct { Interface string `yaml:"interface"` @@ -196,20 +215,18 @@ func TestTheExampleManifestIsTheHostsDoorOnly(t *testing.T) { } `yaml:"health"` } `yaml:"endpoints"` } - example := fencedYAML(t, templateText(t)["templates/factory/GETTING_STARTED.md.tmpl"]) - require.NoError(t, yaml.Unmarshal([]byte(example), &document), - "the example an author copies must be readable YAML") + require.NoError(t, yaml.Unmarshal([]byte(document), &declared), "the manifest must be readable YAML") - require.True(t, document.ModuleIdentity, + require.True(t, declared.ModuleIdentity, "a solution backend presents the solution's identity: the authority takes its workload credential as the actor of every Work Context it issues") - require.Len(t, document.Dependencies, 2, + require.Len(t, declared.Dependencies, 2, "the host's door is the authority interface and its hop, and nothing else") // The interface, required by `interface:` alone so the workspace binds it // to the one authority in scope. check_solution_mesh reads an // interface-only entry as the door; one that also names a service is an // ordinary module edge, and the hop is a door ONLY beside this entry. - authority := document.Dependencies[0] + authority := declared.Dependencies[0] require.Equal(t, "codefly.dev/authority", strings.Split(authority.Interface, "@")[0]) require.Empty(t, authority.Name, "the interface requirement names no service") require.Empty(t, authority.Module, "the interface requirement names no module") @@ -217,16 +234,16 @@ func TestTheExampleManifestIsTheHostsDoorOnly(t *testing.T) { // The authority's hop. `external`: composed by the workspace that composes // this solution, so it consumes no endpoint and derives no mesh allow rule // — it names the edge and opens none. - hop := document.Dependencies[1] + hop := declared.Dependencies[1] require.Equal(t, "authority", hop.Module) require.Equal(t, "hop", hop.Name) require.Equal(t, "external", hop.Kind, "a hop edge that is not external derives a mesh allow rule to the hop's service") // The one endpoint, with the readiness probe the scaffolded backend answers. - require.Len(t, document.Endpoints, 1, "a solution backend exposes one endpoint") - require.Equal(t, "http", document.Endpoints[0].Name) - require.Equal(t, "/health", document.Endpoints[0].Health.Readiness.Path) + require.Len(t, declared.Endpoints, 1, "a solution backend exposes one endpoint") + require.Equal(t, "http", declared.Endpoints[0].Name) + require.Equal(t, "/health", declared.Endpoints[0].Health.Readiness.Path) } // templateActions stand where a value goes, so each becomes a scalar before diff --git a/templates/factory/.gitignore.tmpl b/templates/factory/.gitignore.tmpl index 4460ccc..ef7ed4e 100644 --- a/templates/factory/.gitignore.tmpl +++ b/templates/factory/.gitignore.tmpl @@ -1,6 +1,6 @@ # The compiled backend app -{{ .Service.Name.LowerCase }} +backend # Go workspace (keep the module-level go.mod pinned) go.work diff --git a/templates/factory/GETTING_STARTED.md.tmpl b/templates/factory/GETTING_STARTED.md.tmpl index d83254e..87c4fd0 100644 --- a/templates/factory/GETTING_STARTED.md.tmpl +++ b/templates/factory/GETTING_STARTED.md.tmpl @@ -1,14 +1,14 @@ -# The {{ .Module.Name.LowerCase }} solution's backend +# The {{ .SolutionID }} solution's backend Scaffolded by `codefly.dev/solution-go`. Two directories were written: -- `services/{{ .Service.Name.LowerCase }}` — this service, the solution's entry service. +- `services/backend` — this service, the solution's entry service. - `services/fe-remote` — the `./Page` Module Federation remote. An npm package, not a Codefly service, which is why nothing else writes it. ## What is left, in order -1. `cd services/{{ .Service.Name.LowerCase }} && go build ./... && go test ./...` +1. `cd services/backend && go build ./... && go test ./...` 2. `cd services/fe-remote && npm install && npm test && npm run typecheck` The `@codefly-dev/*` kits come from GitHub Packages, so `NODE_AUTH_TOKEN` must be set to a token with `read:packages` (see `.npmrc`). @@ -44,7 +44,7 @@ service-dependencies: - name: grpc workspace-configuration-dependencies: - authority - - {{ .Module.Name.LowerCase }} + - {{ .SolutionID }} endpoints: - name: http visibility: public diff --git a/templates/factory/openapi.go.tmpl b/templates/factory/openapi.go.tmpl index 8e33e10..8c7565f 100644 --- a/templates/factory/openapi.go.tmpl +++ b/templates/factory/openapi.go.tmpl @@ -20,7 +20,7 @@ const interfaceArtifactPath = "openapi/api.swagger.json" func renderInterfaceArtifact(dir string) ([]byte, error) { return server().InterfaceArtifact(dir, solution.InterfaceInfo{ Title: "{{ .SolutionID }} solution backend", - Description: "{{ .InterfaceDescription }}", + Description: "The {{ .SolutionID }} solution's backend under the authority interface, at the route the authority's edge forwards it on: the routes it serves itself, and the module calls the solution runtime relays as the viewer. A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, and the module's own method policy, gate it.", // No base path: every route already carries /v1/{{ .SolutionID }}, as served. OperationPrefix: "{{ .OperationPrefix }}", Tag: solutionID, From 1f4b021d7234727dfaf898b165fbc82579bbfb8c Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:00:35 -0400 Subject: [PATCH 6/6] fix: what core renders, and the order the CLI really runs in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running `codefly add solution --agent solution-go` against a real CLI built from codefly-dev/cli main found three things no test here could see, because every test rendered this agent's templates with this agent's own context. 1. THE AGENT README KILLED `add solution`. templates/agent/README.md.tmpl opened with `{{ .Service.Name.Title }}`, and `GetAgentInformation` is called BEFORE any service exists — core has run only the agent's Init, so Information carries the agent and nothing else. The call failed with "nil pointer evaluating *resources.ServiceWithCase.Name" and the command died with the module already written. The README describes the AGENT, so it now takes no values at all. 2. GETTING_STARTED IS RENDERED BY CORE, with the same bare Information (base_builder.go, creation mode). The base/ restructure had replaced `codefly-base` with `{{ .SolutionID }}`, a field Information does not carry: "can't evaluate field SolutionID in type *services.Information", again after the module was written. The replacement is now `{{ .Module.Name.LowerCase }}` — the same value, and a field BOTH contexts carry, since the scaffold context embeds Information. 3. THE END-TO-END TEST MODELLED THE WRONG ORDER. It wrote solution.codefly.yaml before Create; `add solution` writes it after. With `--consumes`, a freshly scaffolded solution therefore has an empty consumed_gen.go and its drift check fails until `codefly sync` runs. That failure is TRUE — the backend really does relay nothing its manifest declares — so the test now pins it: exactly one test may fail, it must be that one, and it must name the command that fixes it. Then it runs the agent's Sync and requires the tree green. advertisement_test.go renders both core-rendered templates the way core renders them. TestGeneratedTemplatesRenderWithTheScaffoldContext now EXECUTES each replacement against the scaffold context instead of comparing it to a list of fields that happen to exist today. Verified end to end: `codefly add solution notes --agent solution-go --consumes connect:documents/documents/grpc` scaffolds, `codefly doctor solution --dir` reports "conforms to the solution shape" at exit 0, and all three core-solutions gates pass at exit 0 on that tree, with the runtime-pin gate confirmed to still have teeth on it. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 24 ++++- advertisement_test.go | 104 ++++++++++++++++++++++ base/service.generation.codefly.yaml | 30 ++++++- base_test.go | 10 ++- endtoend_test.go | 82 +++++++++++++---- factory_test.go | 27 ++++-- scaffold_test.go | 33 +++++-- templates/agent/README.md.tmpl | 15 ++-- templates/factory/GETTING_STARTED.md.tmpl | 4 +- templates/factory/go.mod.tmpl | 2 +- templates/factory/main.go.tmpl | 4 +- templates/factory/openapi.go.tmpl | 6 +- templates/factory/operations.go.tmpl | 2 +- 13 files changed, 288 insertions(+), 55 deletions(-) create mode 100644 advertisement_test.go diff --git a/AGENTS.md b/AGENTS.md index 04b7677..b9e3494 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,15 +53,33 @@ derived from the name exactly as `scaffold.go` derives it for a real solution: | in `base/` | becomes | derived by | |---|---|---| -| `codefly-base` | `{{ .SolutionID }}` | `Module.Name.LowerCase` | -| `Codefly Base` | `{{ .Title }}` | `titleFrom` | -| `CodeflyBase` | `{{ .OperationPrefix }}` | `aliasField` | +| `codefly-base` | `{{ .Module.Name.LowerCase }}` | core's own `Information` | +| `Codefly Base` | `{{ .Title }}` | `scaffold.go` `titleFrom` | +| `CodeflyBase` | `{{ .OperationPrefix }}` | `scaffold.go` `aliasField` | Order matters and no `from` may be a substring of an earlier `to`. `TestGeneratedTemplatesCarryNoBaseSolutionName` fails if any spelling of `codefly-base` survives into a template, which is what catches a new capitalisation introduced in `base/` that no replacement covers. +**The solution id is spelled `.Module.Name.LowerCase`, never `.SolutionID`.** +They are the same value — `scaffold.go` derives `SolutionID` from exactly that +field — but not renderable in the same places. Two files in a generated tree +are rendered by CORE rather than by this agent, each with a bare +`*services.Information` that carries only `Service`, `Module` and `Agent`: + +| rendered by core | when | where | +|---|---|---| +| `templates/factory/GETTING_STARTED.md` | builder Load, creation mode | `core/agents/services/base_builder.go` | +| `templates/agent/README.md` | `GetAgentInformation`, before any service exists | this repo's `service.go` | + +`{{ .SolutionID }}` there is not an empty render but a hard failure — *can't +evaluate field SolutionID in type \*services.Information* — and +`codefly add solution` dies with the module already written. The agent README +takes no values at all for the same reason: at advertisement time `Service` and +`Module` are themselves nil. `advertisement_test.go` renders both the way core +renders them. + `backend` is **not** replaced. A solution's entry service is named `backend` by the shape's own definition — the CLI's `solutionEntryService` and this agent's `backendService` are both that constant — so it is not a parameter, and it diff --git a/advertisement_test.go b/advertisement_test.go new file mode 100644 index 0000000..46af7b9 --- /dev/null +++ b/advertisement_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "strings" + "testing" + + "github.com/codefly-dev/core/agents/services" + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" + "github.com/codefly-dev/core/resources" + "github.com/codefly-dev/core/shared" + "github.com/codefly-dev/core/templates" + "github.com/stretchr/testify/require" +) + +// THE ADVERTISEMENT IS ASKED FOR BEFORE A SERVICE EXISTS, and that is the +// whole reason this file is here. +// +// `codefly add service` calls GetAgentInformation to print what the agent +// scaffolds — at that moment nothing has been created, so core has only run +// the agent's Init and s.Information carries the AGENT and nothing else +// (core/agents/services/base.go: `s.Information = &Information{Agent: +// s.Agent}`). Service and Module are nil until Load runs against a service +// directory that exists. +// +// A README template that reaches for {{ .Service.Name.Title }} therefore does +// not render a blank — it fails the whole call with "nil pointer evaluating +// *resources.ServiceWithCase.Name", and `codefly add solution` dies after +// having already written the module, leaving a half-built tree behind. This +// agent shipped exactly that, and every test here passed: they all supply an +// Information built from a real identity, which is the state at Create time +// and NOT the state at advertisement time. +func TestTheAdvertisementRendersBeforeAnyServiceExists(t *testing.T) { + // NewService and nothing else: the state core leaves the agent in when it + // asks what the agent is, before Load. + svc := NewService() + require.Nil(t, svc.Base.Information, + "this test is only honest while the agent carries no service here; if core now supplies one, make this test build the state it supplies") + + information, err := svc.GetAgentInformation(context.Background(), &agentv0.AgentInformationRequest{}) + require.NoError(t, err, "GetAgentInformation must render with no service loaded: this is what `codefly add service` calls first") + require.NotEmpty(t, information.ReadMe) + require.Contains(t, information.ReadMe, agent.Name, + "the README a caller is shown must name the agent it is reading about") +} + +// TestTheAgentReadmeNeedsNoServiceContext is the same rule one step earlier, +// stated against the template rather than the call: the agent README describes +// THE AGENT, so a service-shaped or module-shaped value in it has no source to +// come from. Reading the template says why the test above holds, rather than +// leaving it a fact about one rendering. +func TestTheAgentReadmeNeedsNoServiceContext(t *testing.T) { + raw, err := readmeFS.ReadFile("templates/agent/README.md.tmpl") + require.NoError(t, err) + content := string(raw) + for _, reach := range []string{".Service", ".Module", ".Information"} { + require.NotContains(t, content, reach, + "templates/agent/README.md.tmpl is rendered with the agent's own Information, where %s is nil until a service is loaded", reach) + } + require.NotContains(t, content, "{{", + "the agent README takes no values at all: keep it prose, so there is nothing left to be nil") + require.True(t, strings.Contains(content, agent.Name), + "the agent README must name the agent") +} + +// THE SECOND THING CORE RENDERS WITH A BARE Information, and the one that +// cost a half-built module. +// +// templates/factory/GETTING_STARTED.md is rendered TWICE, by two callers with +// two different contexts: +// +// - by this agent's Create, with the scaffold context, into the service +// directory (scaffold.go); +// - by CORE, at builder Load in creation mode, with s.Information and +// nothing else, to print to the author — core/agents/services/ +// base_builder.go does it by that exact path, on every agent. +// +// So this one template is constrained to the fields Information carries. A +// `{{ .SolutionID }}` in it is not an empty render: text/template fails the +// call with "can't evaluate field SolutionID in type *services.Information", +// core turns that into "builder load failed", and `codefly add solution` dies +// AFTER `add module` has already written the module — the half-built tree the +// command's own design goes out of its way to avoid. +// +// base/service.generation.codefly.yaml therefore spells the solution id +// `.Module.Name.LowerCase`, which both contexts carry. This test is what holds +// it there. +func TestTheGettingStartedRendersTheWayCoreRendersIt(t *testing.T) { + // Exactly core's call: same FS, same path, same context type. + information := &services.Information{ + Service: resources.ToServiceWithCase(&resources.ServiceIdentity{Name: backendService, Module: "notes", Workspace: "workspace"}), + Module: resources.ToModuleWithCase(&resources.ServiceIdentity{Name: backendService, Module: "notes", Workspace: "workspace"}), + Agent: agent, + } + rendered, err := templates.ApplyTemplateFrom(context.Background(), shared.Embed(factoryFS), + "templates/factory/GETTING_STARTED.md", information) + require.NoError(t, err, + "core renders this template with a bare *services.Information at builder Load: a field Information does not carry fails `codefly add solution` after the module is already written") + + // A render that succeeds but says nothing is the other way to be wrong: + // the solution's name is what the document is about. + require.Contains(t, rendered, "notes", "the rendered document must name the solution") + require.NotContains(t, rendered, "", "every action must resolve under Information, not render empty") +} diff --git a/base/service.generation.codefly.yaml b/base/service.generation.codefly.yaml index a200387..0b333dc 100644 --- a/base/service.generation.codefly.yaml +++ b/base/service.generation.codefly.yaml @@ -17,9 +17,31 @@ # which are derived from it exactly as scaffold.go derives them for a real # solution: # -# codefly-base the solution id -> SolutionID (scaffold.go: Module.Name.LowerCase) -# Codefly Base the placeholder title -> Title (titleFrom) -# CodeflyBase the operation prefix -> OperationPrefix (aliasField) +# codefly-base the solution id -> Module.Name.LowerCase +# Codefly Base the placeholder title -> Title (scaffold.go titleFrom) +# CodeflyBase the operation prefix -> OperationPrefix (scaffold.go aliasField) +# +# THE SOLUTION ID IS SPELLED `.Module.Name.LowerCase`, NOT `.SolutionID`, AND +# THE DIFFERENCE IS NOT COSMETIC — they are the same value (scaffold.go derives +# SolutionID from exactly that field), but they are not renderable in the same +# places. templates/factory/GETTING_STARTED.md is rendered TWICE and by two +# different callers: +# +# - by this agent's Create, with the scaffold context, into the service +# directory; +# - by CORE, at builder Load in creation mode, with a bare +# *services.Information and nothing else +# (core/agents/services/base_builder.go: ApplyTemplateFrom(..., +# "templates/factory/GETTING_STARTED.md", s.Information)) — which is what +# `codefly add service` prints to the author. +# +# Information carries Service, Module and Agent. It has no SolutionID, so +# `{{ .SolutionID }}` there is not an empty render but a hard failure — +# "can't evaluate field SolutionID in type *services.Information" — and the +# whole `codefly add solution` dies after the module has already been written. +# `.Module.Name.LowerCase` is a field BOTH contexts carry (the scaffold context +# embeds *services.Information), so one spelling renders under both. +# advertisement_test.go renders this template the way core does. # # The fourth, `codefly_base`, is the JavaScript identifier and appears only in # templates/page, which is not generated from here (it is an npm package, not a @@ -39,7 +61,7 @@ replacements: - from: Codefly Base to: "{{ .Title }}" - from: codefly-base - to: "{{ .SolutionID }}" + to: "{{ .Module.Name.LowerCase }}" ignores: # This file. - service.generation.codefly.yaml diff --git a/base_test.go b/base_test.go index 76b5272..54cf0a3 100644 --- a/base_test.go +++ b/base_test.go @@ -246,10 +246,18 @@ func goToolchain(t *testing.T) string { // — it is a separate module in a temporary directory, and a workspace would // try to resolve it against this repository's. func run(t *testing.T, goTool, dir string, args ...string) { + t.Helper() + output, err := attempt(t, goTool, dir, args...) + require.NoErrorf(t, err, "`go %s` in %s:\n%s", strings.Join(args, " "), dir, output) +} + +// attempt is run without the assertion, for the one caller that expects a +// failure and has something to say about which one it is. +func attempt(t *testing.T, goTool, dir string, args ...string) (string, error) { t.Helper() command := exec.Command(goTool, args...) command.Dir = dir command.Env = append(os.Environ(), "GOWORK=off") output, err := command.CombinedOutput() - require.NoErrorf(t, err, "`go %s` in the staged base/:\n%s", strings.Join(args, " "), output) + return string(output), err } diff --git a/endtoend_test.go b/endtoend_test.go index 7d909c5..e4a13a9 100644 --- a/endtoend_test.go +++ b/endtoend_test.go @@ -3,6 +3,7 @@ package main import ( "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -19,24 +20,31 @@ import ( // into Go that does not compile, or an agent-written file that disagrees with // what the templates expect, passes every other test here and fails this one. // -// It is what caught both defects this file was added with: a solution that -// consumed anything was born red, because the runtime refuses a consumed -// module declaring no method, and the scaffolded drift check asserted an -// equality the reference solution does not hold (api.consumes names every -// module the solution reaches anywhere; this backend relays a subset). +// IT RUNS IN THE ORDER `codefly add solution` RUNS IN, and that order is the +// whole subtlety: the agent's Create goes FIRST and the solution's own +// documents are written after it returns. Writing the manifest first — which +// this file used to do — proves a tree no user ever has. // TestAScaffoldedSolutionBuildsAndPassesItsOwnGates is that run, for a // solution that consumes modules and one that consumes none. Both are real // states of `codefly add solution`: with `--consumes` and without. func TestAScaffoldedSolutionBuildsAndPassesItsOwnGates(t *testing.T) { - for name, solutionManifest := range map[string]string{ - "consumes nothing": emptyManifest, - "consumes modules": consumingManifest, + for name, solution := range map[string]struct { + manifest string + consumes bool + }{ + "consumes nothing": {manifest: emptyManifest}, + "consumes modules": {manifest: consumingManifest, consumes: true}, } { t.Run(name, func(t *testing.T) { goTool := goToolchain(t) - backend := filepath.Join(scaffoldInto(t, "notes", solutionManifest), "services", backendService) - completeAsTheCLIDoes(t, backend) + + // Create, with no solution manifest in sight: `add solution` + // scaffolds both services through the agents' own Create flows and + // writes the solution's documents only once both exist. + root := scaffoldInto(t, "notes", "") + backend := filepath.Join(root, "services", backendService) + completeAsTheCLIDoes(t, root, solution.manifest) // `go build` with the default -mod=readonly and no proxy: the // scaffolded go.mod and go.sum are tidy as generated, so a fresh @@ -44,19 +52,54 @@ func TestAScaffoldedSolutionBuildsAndPassesItsOwnGates(t *testing.T) { // for anything the agent's own module graph has not already // resolved. run(t, goTool, backend, "build", "./...") - // The suite the solution ships with: its consumption drift check, - // its own refusal of a module edge (SP-SOL-04), and the interface - // document, which must already be what `go generate ./...` would - // render — nobody has run a generator in this tree. + + if !solution.consumes { + // The suite the solution ships with: its consumption drift + // check, its own refusal of a module edge (SP-SOL-04), and the + // interface document, which must already be what + // `go generate ./...` would render — nobody has run a + // generator in this tree. + run(t, goTool, backend, "test", "./...") + return + } + + // WITH --consumes, A FRESHLY SCAFFOLDED SOLUTION IS RED ON EXACTLY + // ONE TEST, AND THAT IS THE TRUTH RATHER THAN A DEFECT. + // + // consumed_gen.go is rendered from api.consumes, which does not + // exist when Create runs, so it is rendered empty (builder.go says + // so). Until `codefly sync` has run, this backend relays nothing + // its solution declares — and the drift check saying so is the + // whole reason that check exists. The alternative is a backend + // that silently reaches nothing. + // + // This is pinned rather than tolerated: if the failure ever + // becomes a different one, or spreads to a second test, this fails + // and names it. If `codefly add solution` ever syncs before it + // returns, the tree is green here and this fails too — which is + // the moment to delete this branch. + output, err := attempt(t, goTool, backend, "test", "./...") + require.Error(t, err, "a solution that consumes modules has an empty consumed_gen.go until `codefly sync` runs: the drift check must say so") + require.Contains(t, output, "--- FAIL: TestTheGeneratedConsumptionMatchesTheManifest") + require.Contains(t, output, "run `codefly sync` and commit it", + "the failure must carry the one command that fixes it") + require.Equal(t, 1, strings.Count(output, "--- FAIL:"), + "exactly one test may fail on a freshly scaffolded solution; everything else is a defect:\n%s", output) + + // And the recovery is the whole of it: the agent's own Sync, which + // is what `codefly sync service /backend` calls. + syncAsTheCLIDoes(t, root, "notes") run(t, goTool, backend, "test", "./...") }) } } -// completeAsTheCLIDoes writes the two files `codefly add solution` writes into -// the service directory AFTER this agent's Create returns, and that this agent -// must not write (shape_test.go holds it to that): +// completeAsTheCLIDoes writes what `codefly add solution` writes AFTER this +// agent's Create returns, and that this agent must not write (shape_test.go +// holds it to that): // +// - solution.codefly.yaml, the solution's own manifest, one directory above +// the service. api.consumes lives there and nowhere else. // - solution.frontend.codefly.yaml, which the scaffolded main.go embeds. A // backend does not compile without it, by design: the runtime serves this // block and the CLI delivers it in the presence document it signs, so one @@ -66,8 +109,11 @@ func TestAScaffoldedSolutionBuildsAndPassesItsOwnGates(t *testing.T) { // GETTING_STARTED.md's own example rather than written here, so what this // test proves is that THE DOCUMENTED MANIFEST is the one that works. An // example that drifts from what the CLI writes fails here. -func completeAsTheCLIDoes(t *testing.T, backend string) { +func completeAsTheCLIDoes(t *testing.T, root, manifest string) { t.Helper() + require.NoError(t, os.WriteFile(filepath.Join(root, "solution.codefly.yaml"), []byte(manifest), 0o600)) + + backend := filepath.Join(root, "services", backendService) require.NoError(t, os.WriteFile(filepath.Join(backend, "solution.frontend.codefly.yaml"), []byte("title: Notes\norder: 60\nexposed_module: ./Page\nhost_contract: 1\n"), 0o600)) diff --git a/factory_test.go b/factory_test.go index 32bed9e..f7b3351 100644 --- a/factory_test.go +++ b/factory_test.go @@ -8,8 +8,10 @@ import ( "sort" "strings" "testing" + "text/template" "github.com/codefly-dev/core/agents/generator" + "github.com/codefly-dev/core/agents/services" "github.com/codefly-dev/core/generation" "github.com/codefly-dev/core/resources" "github.com/stretchr/testify/require" @@ -82,15 +84,28 @@ func TestGeneratedTemplatesCarryNoBaseSolutionName(t *testing.T) { // fails at Create, in a user's terminal. Every action in the generated tree // must be one this agent can fill, which the scaffold tests below prove by // rendering the whole tree; this states the narrower rule the replacements -// must obey. +// must obey, and states it by RENDERING each one rather than by listing the +// fields that happen to exist today — a list goes stale silently, and the +// thing being checked is whether the action resolves. func TestGeneratedTemplatesRenderWithTheScaffoldContext(t *testing.T) { - known := map[string]bool{ - "{{ .SolutionID }}": true, "{{ .Title }}": true, "{{ .OperationPrefix }}": true, - } + identity := &resources.ServiceIdentity{Name: backendService, Module: "notes", Workspace: "workspace"} + fill, err := newScaffold(&services.Information{ + Service: resources.ToServiceWithCase(identity), + Module: resources.ToModuleWithCase(identity), + Agent: agent, + }) + require.NoError(t, err) + for _, replacement := range baseGeneration(t).Replacements { - require.Containsf(t, known, replacement.To, - "base/service.generation.codefly.yaml replaces %q with %q, which the scaffold context does not carry", + action, parseErr := template.New("replacement").Parse(replacement.To) + require.NoErrorf(t, parseErr, "%q is not a template", replacement.To) + var rendered strings.Builder + require.NoErrorf(t, action.Execute(&rendered, fill), + "base/service.generation.codefly.yaml replaces %q with %q, which the scaffold context cannot fill", replacement.From, replacement.To) + require.NotEmptyf(t, strings.TrimSpace(rendered.String()), + "%q renders empty: every scaffolded solution would carry a blank where %q stood", + replacement.To, replacement.From) } } diff --git a/scaffold_test.go b/scaffold_test.go index bff96d8..8f36296 100644 --- a/scaffold_test.go +++ b/scaffold_test.go @@ -28,6 +28,22 @@ func scaffoldInto(t *testing.T, module, manifest string) (root string) { if manifest != "" { require.NoError(t, os.WriteFile(filepath.Join(root, "solution.codefly.yaml"), []byte(manifest), 0o600)) } + builder := builderFor(t, root, module) + ctx := context.Background() + context, err := newScaffold(builder.Information) + require.NoError(t, err) + require.NoError(t, builder.scaffoldSolution(ctx, context)) + require.NoError(t, builder.writeGeneratedConsumption(ctx)) + require.NoError(t, builder.writeInterfaceArtifact(ctx, context, "0.0.0")) + return root +} + +// builderFor is this agent loaded against /services/backend, the way +// core loads it before any lifecycle call. It is shared by scaffoldInto and by +// syncAsTheCLIDoes, so what Create runs and what Sync runs are the same agent +// against the same tree. +func builderFor(t *testing.T, root, module string) *Builder { + t.Helper() location := filepath.Join(root, "services", backendService) require.NoError(t, os.MkdirAll(location, 0o750)) @@ -40,15 +56,16 @@ func scaffoldInto(t *testing.T, module, manifest string) (root string) { Module: resources.ToModuleWithCase(identity), Agent: agent, } + return NewBuilder(svc) +} - builder := NewBuilder(svc) - ctx := context.Background() - context, err := newScaffold(svc.Base.Information) - require.NoError(t, err) - require.NoError(t, builder.scaffoldSolution(ctx, context)) - require.NoError(t, builder.writeGeneratedConsumption(ctx)) - require.NoError(t, builder.writeInterfaceArtifact(ctx, context, "0.0.0")) - return root +// syncAsTheCLIDoes is `codefly sync service /backend`: the agent's +// Sync, which re-renders consumed_gen.go from the solution manifest as it now +// stands. It is a distinct step from Create because the manifest does not +// exist when Create runs — see TestAScaffoldedSolutionBuildsAndPassesItsOwnGates. +func syncAsTheCLIDoes(t *testing.T, root, module string) { + t.Helper() + require.NoError(t, builderFor(t, root, module).writeGeneratedConsumption(context.Background())) } // emptyManifest is a solution manifest that consumes nothing — what `codefly diff --git a/templates/agent/README.md.tmpl b/templates/agent/README.md.tmpl index f1f3ff3..f86db44 100644 --- a/templates/agent/README.md.tmpl +++ b/templates/agent/README.md.tmpl @@ -1,13 +1,16 @@ -# {{ .Service.Name.Title }} — a solution backend +# solution-go — a solution backend -This service is the entry service of the `{{ .Module.Name.LowerCase }}` solution: the -workload a host installs, built on +`codefly add service --agent solution-go` scaffolds the **entry service +of a solution**: the workload a host installs, built on [`solution-runtime-go`](https://github.com/codefly-dev/solution-runtime-go). +`codefly add solution` scaffolds the whole shape and calls this agent for that +service. The runtime does everything a solution needs identically — it presents what the solution is, answers `/health`, serves the page's calls, and relays the page's module calls to the authority's hop **as the viewer**, where each module -authorizes the call. What lives here is this solution's own composition: +authorizes the call. What lives in the scaffolded service is this solution's +own composition: | file | what it says | |---|---| @@ -20,7 +23,7 @@ authorizes the call. What lives here is this solution's own composition: A solution reaches every module through the authority's hop, which checks the call. It therefore declares **no** `service-dependencies` edge on a module it consumes: Codefly renders such an edge into a mesh allow rule straight to that -module's service, past the check. `consumes_test.go` holds this service to that. +module's service, past the check. `consumes_test.go` holds the service to that. -The page this solution serves is `../fe-remote` — the `./Page` Module +The page the solution serves is `../fe-remote` — the `./Page` Module Federation remote, an npm package rather than a Codefly service. diff --git a/templates/factory/GETTING_STARTED.md.tmpl b/templates/factory/GETTING_STARTED.md.tmpl index 87c4fd0..24de805 100644 --- a/templates/factory/GETTING_STARTED.md.tmpl +++ b/templates/factory/GETTING_STARTED.md.tmpl @@ -1,4 +1,4 @@ -# The {{ .SolutionID }} solution's backend +# The {{ .Module.Name.LowerCase }} solution's backend Scaffolded by `codefly.dev/solution-go`. Two directories were written: @@ -44,7 +44,7 @@ service-dependencies: - name: grpc workspace-configuration-dependencies: - authority - - {{ .SolutionID }} + - {{ .Module.Name.LowerCase }} endpoints: - name: http visibility: public diff --git a/templates/factory/go.mod.tmpl b/templates/factory/go.mod.tmpl index 790a825..58ec3de 100644 --- a/templates/factory/go.mod.tmpl +++ b/templates/factory/go.mod.tmpl @@ -1,4 +1,4 @@ -module {{ .SolutionID }}/backend +module {{ .Module.Name.LowerCase }}/backend go 1.27.3 diff --git a/templates/factory/main.go.tmpl b/templates/factory/main.go.tmpl index 754db35..e14dca8 100644 --- a/templates/factory/main.go.tmpl +++ b/templates/factory/main.go.tmpl @@ -27,9 +27,9 @@ import ( var frontendDeclaration []byte // solutionID is this solution's ONE name: the id the host's presence document -// names it by, the alias the authority's edge routes it at (/v1/{{ .SolutionID }}), +// names it by, the alias the authority's edge routes it at (/v1/{{ .Module.Name.LowerCase }}), // the principal the authority issues to, and the Codefly module name. -const solutionID = "{{ .SolutionID }}" +const solutionID = "{{ .Module.Name.LowerCase }}" // manifest is what this solution is. // diff --git a/templates/factory/openapi.go.tmpl b/templates/factory/openapi.go.tmpl index 8c7565f..3a1c490 100644 --- a/templates/factory/openapi.go.tmpl +++ b/templates/factory/openapi.go.tmpl @@ -19,9 +19,9 @@ const interfaceArtifactPath = "openapi/api.swagger.json" // renderInterfaceArtifact renders the document for the backend rooted at dir. func renderInterfaceArtifact(dir string) ([]byte, error) { return server().InterfaceArtifact(dir, solution.InterfaceInfo{ - Title: "{{ .SolutionID }} solution backend", - Description: "The {{ .SolutionID }} solution's backend under the authority interface, at the route the authority's edge forwards it on: the routes it serves itself, and the module calls the solution runtime relays as the viewer. A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, and the module's own method policy, gate it.", - // No base path: every route already carries /v1/{{ .SolutionID }}, as served. + Title: "{{ .Module.Name.LowerCase }} solution backend", + Description: "The {{ .Module.Name.LowerCase }} solution's backend under the authority interface, at the route the authority's edge forwards it on: the routes it serves itself, and the module calls the solution runtime relays as the viewer. A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, and the module's own method policy, gate it.", + // No base path: every route already carries /v1/{{ .Module.Name.LowerCase }}, as served. OperationPrefix: "{{ .OperationPrefix }}", Tag: solutionID, }) diff --git a/templates/factory/operations.go.tmpl b/templates/factory/operations.go.tmpl index fe43105..baafa3a 100644 --- a/templates/factory/operations.go.tmpl +++ b/templates/factory/operations.go.tmpl @@ -27,7 +27,7 @@ const route = "/v1/" + solutionID // Callers: "The page's Notes view.", // Behavior: "Answers the viewer's own notes, newest first, 50 at a time.", // Response: notesResponse{}, -// Policy: solution.Requires("{{ .SolutionID }}.note", "read"), +// Policy: solution.Requires("{{ .Module.Name.LowerCase }}.note", "read"), // Handler: listNotes, // } //