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/.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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b9e3494 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,131 @@ +# 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` | `{{ .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 +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/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/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/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..0b333dc --- /dev/null +++ b/base/service.generation.codefly.yaml @@ -0,0 +1,101 @@ +# 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 -> 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 +# 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: "{{ .Module.Name.LowerCase }}" +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..54cf0a3 --- /dev/null +++ b/base_test.go @@ -0,0 +1,263 @@ +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() + 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() + return string(output), err +} 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/endtoend_test.go b/endtoend_test.go new file mode 100644 index 0000000..e4a13a9 --- /dev/null +++ b/endtoend_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// THE VERIFICATION BAR, run rather than asserted. +// +// 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 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, 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) + + // 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 + // 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", "./...") + + 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 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 +// 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, 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)) + + // 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). + 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(header+declared+"\n"), 0o600)) +} diff --git a/factory_test.go b/factory_test.go new file mode 100644 index 0000000..f7b3351 --- /dev/null +++ b/factory_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "io/fs" + "os" + "path/filepath" + "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" +) + +// 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, 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) { + 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 { + 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) + } +} + +// 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/go.mod b/go.mod new file mode 100644 index 0000000..709763b --- /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 + 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.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 + 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..30fcf71 --- /dev/null +++ b/main.go @@ -0,0 +1,116 @@ +// 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. +// +// 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 + +// 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). +// +// 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 + +//go:embed templates/builder +var builderFS embed.FS + +//go:embed templates/deployment +var deploymentFS embed.FS 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") +} diff --git a/scaffold.go b/scaffold.go new file mode 100644 index 0000000..4dbf640 --- /dev/null +++ b/scaffold.go @@ -0,0 +1,251 @@ +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 + // 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" + +// 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_$]*$`) + +// 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, + 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..8f36296 --- /dev/null +++ b/scaffold_test.go @@ -0,0 +1,297 @@ +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)) + } + 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)) + + 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, + } + return NewBuilder(svc) +} + +// 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 +// 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..40cb892 --- /dev/null +++ b/shape_test.go @@ -0,0 +1,305 @@ +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" + "gopkg.in/yaml.v3" +) + +// 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. +// +// 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) { + 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) { + prose := withoutFencedExamples(name, content) + for _, declaration := range declarations { + 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 +// 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) { + for name, content := range templateText(t) { + if !strings.Contains(content, "service-dependencies") { + continue + } + 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) { + 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"` + 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"` + } + require.NoError(t, yaml.Unmarshal([]byte(document), &declared), "the manifest must be readable YAML") + + 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, 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 := 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") + + // 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 := 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, 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 +// 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 +// 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..f86db44 --- /dev/null +++ b/templates/agent/README.md.tmpl @@ -0,0 +1,29 @@ +# solution-go — a solution backend + +`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 in the scaffolded service 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 the service to that. + +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/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..ef7ed4e --- /dev/null +++ b/templates/factory/.gitignore.tmpl @@ -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/templates/factory/GETTING_STARTED.md.tmpl b/templates/factory/GETTING_STARTED.md.tmpl new file mode 100644 index 0000000..24de805 --- /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/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 + - {{ .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..f0f63c8 --- /dev/null +++ b/templates/factory/consumes.go.tmpl @@ -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/templates/factory/consumes_test.go.tmpl b/templates/factory/consumes_test.go.tmpl new file mode 100644 index 0000000..dc0a947 --- /dev/null +++ b/templates/factory/consumes_test.go.tmpl @@ -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/templates/factory/go.mod.tmpl b/templates/factory/go.mod.tmpl new file mode 100644 index 0000000..58ec3de --- /dev/null +++ b/templates/factory/go.mod.tmpl @@ -0,0 +1,80 @@ +module {{ .Module.Name.LowerCase }}/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/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..e14dca8 --- /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/{{ .Module.Name.LowerCase }}), +// the principal the authority issues to, and the Codefly module name. +const solutionID = "{{ .Module.Name.LowerCase }}" + +// 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..3a1c490 --- /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: "{{ .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/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..baafa3a --- /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("{{ .Module.Name.LowerCase }}.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..cd16e67 --- /dev/null +++ b/templates/page/rsbuild.config.ts.tmpl @@ -0,0 +1,72 @@ +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; + +// 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/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"] } }, + }, +});