From 3e5935340505dc70ac4686cbc4d556fcbb60a8df Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 10:52:54 -0700 Subject: [PATCH 01/14] Add system Nexus WIT generation --- Makefile | 44 ++++++- cmd/gen-system-nexus-wit/main.go | 198 +++++++++++++++++++++++++++++++ nexus/temporal-system.wit | 77 ++++++++++++ 3 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 cmd/gen-system-nexus-wit/main.go create mode 100644 nexus/temporal-system.wit diff --git a/Makefile b/Makefile index ea42652e9..9044d1726 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs install: grpc-install api-linter-install buf-install # Run all linters and compile proto files. -proto: grpc http-api-docs nexus-rpc-yaml +proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit ######################################################################## ##### Variables ###### @@ -17,7 +17,9 @@ GOPATH := $(shell go env GOPATH) endif GOBIN := $(if $(shell go env GOBIN),$(shell go env GOBIN),$(GOPATH)/bin) -PATH := $(GOBIN):$(PATH) +CARGO_HOME ?= $(HOME)/.cargo +CARGO_BIN := $(CARGO_HOME)/bin +PATH := $(GOBIN):$(CARGO_BIN):$(PATH) STAMPDIR := .stamp COLOR := "\e[1;36m%s\e[0m\n" @@ -33,8 +35,15 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT) OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload +SYSTEM_NEXUS_OUT := nexus +SYSTEM_NEXUS_WIT := $(SYSTEM_NEXUS_OUT)/temporal-system.wit +SYSTEM_NEXUS_DESCRIPTOR := $(PROTO_OUT)/system-nexus/temporal_api.bin +SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) +GO_BUILD_CACHE ?= $(abspath $(PROTO_OUT)/go-build-cache) +NEXUS_API_GEN ?= nexus-api-gen + $(PROTO_OUT): - mkdir $(PROTO_OUT) + mkdir -p $(PROTO_OUT) ##### Compile proto files for go ##### grpc: buf-lint api-linter buf-breaking clean go-grpc fix-path @@ -106,7 +115,7 @@ api-linter: @api-linter --set-exit-status $(PROTO_IMPORTS) --config $(PROTO_ROOT)/api-linter.yaml --output-format json $(PROTO_FILES) | gojq -r 'map(select(.problems != []) | . as $$file | .problems[] | {rule: .rule_doc_uri, location: "\($$file.file_path):\(.location.start_position.line_number)"}) | group_by(.rule) | .[] | .[0].rule + ":\n" + (map("\t" + .location) | join("\n"))' $(STAMPDIR): - mkdir $@ + mkdir -p $@ $(STAMPDIR)/buf-mod-prune: $(STAMPDIR) buf.yaml printf $(COLOR) "Pruning buf module" @@ -137,6 +146,33 @@ nexus-rpc-yaml-install: printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..." @cd cmd/protoc-gen-nexus-rpc-yaml && go install . +##### Compile system Nexus WIT files ##### +system-nexus-wit: $(SYSTEM_NEXUS_WIT) + +$(SYSTEM_NEXUS_DESCRIPTOR): $(PROTO_FILES) | $(PROTO_OUT) + printf $(COLOR) "Build Temporal API descriptor set for system Nexus WIT..." + mkdir -p $(@D) + protoc -I $(PROTO_ROOT) \ + --include_imports \ + --include_source_info \ + --descriptor_set_out=$@ \ + $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) + +$(SYSTEM_NEXUS_WIT): $(SYSTEM_NEXUS_DESCRIPTOR) cmd/gen-system-nexus-wit/main.go $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) | $(STAMPDIR)/nexus-api-gen-install + printf $(COLOR) "Generate system Nexus WIT..." + GOCACHE=$(GO_BUILD_CACHE) go run cmd/gen-system-nexus-wit/main.go \ + --descriptors $(SYSTEM_NEXUS_DESCRIPTOR) \ + --nexus-api-gen $(NEXUS_API_GEN) \ + --output $@ \ + $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) + +$(STAMPDIR)/nexus-api-gen-install: | $(STAMPDIR) + printf $(COLOR) "Install nexus-api-gen if missing..." + command -v $(NEXUS_API_GEN) >/dev/null || CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/temporalio/nexus-api-gen + touch $@ + +nexus-api-gen-install: $(STAMPDIR)/nexus-api-gen-install + ##### Clean ##### clean: printf $(COLOR) "Delete generated go files..." diff --git a/cmd/gen-system-nexus-wit/main.go b/cmd/gen-system-nexus-wit/main.go new file mode 100644 index 000000000..865b5f67b --- /dev/null +++ b/cmd/gen-system-nexus-wit/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +var ( + exposedTagRE = regexp.MustCompile(`\btags\s*=\s*"exposed"`) + packageRE = regexp.MustCompile(`^\s*package\s+([A-Za-z0-9_.]+)\s*;`) + rpcRE = regexp.MustCompile(`^\s*rpc\s+([A-Za-z0-9_]+)\s*\(`) + serviceRE = regexp.MustCompile(`^\s*service\s+([A-Za-z0-9_]+)\s*\{`) +) + +func main() { + var descriptors string + var nexusAPIGen string + var output string + flag.StringVar(&descriptors, "descriptors", "", "protobuf descriptor set") + flag.StringVar(&nexusAPIGen, "nexus-api-gen", "nexus-api-gen", "path to nexus-api-gen") + flag.StringVar(&output, "output", "", "output WIT file") + flag.Parse() + + if descriptors == "" || output == "" || flag.NArg() == 0 { + fmt.Fprintf(os.Stderr, "usage: gen_system_nexus_wit --descriptors DESCRIPTORS [--nexus-api-gen PATH] --output OUTPUT PROTO...\n") + os.Exit(2) + } + + rpcs, err := exposedRPCs(flag.Args()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if len(rpcs) == 0 { + fmt.Fprintln(os.Stderr, "no proto RPCs are marked as exposed Nexus operations") + os.Exit(1) + } + + tempDir, err := os.MkdirTemp("", "system-nexus-wit-*") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + + tempOutput := filepath.Join(tempDir, "system-nexus.wit") + var input string + if _, err := os.Stat(output); err == nil { + if err := copyFile(output, tempOutput); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + input = tempOutput + } else if !os.IsNotExist(err) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + for _, rpc := range rpcs { + if err := runAddRPC(nexusAPIGen, descriptors, rpc, tempOutput, input); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + input = tempOutput + } + + if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := copyFile(tempOutput, output); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func exposedRPCs(paths []string) ([]string, error) { + var rpcs []string + for _, path := range paths { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var packageName string + var serviceName string + var serviceDepth int + var rpcName string + var rpcDepth int + var rpcIsExposed bool + + for _, rawLine := range strings.Split(string(content), "\n") { + line := stripLineComment(rawLine) + + if packageName == "" { + if match := packageRE.FindStringSubmatch(line); match != nil { + packageName = match[1] + } + } + + if serviceName == "" { + if match := serviceRE.FindStringSubmatch(line); match != nil { + serviceName = match[1] + serviceDepth = braceDelta(line) + } + continue + } + + if rpcName == "" { + if match := rpcRE.FindStringSubmatch(line); match != nil { + rpcName = match[1] + rpcDepth = braceDelta(line) + rpcIsExposed = exposedTagRE.MatchString(line) + if rpcDepth <= 0 { + if rpcIsExposed { + rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) + } + rpcName = "" + rpcDepth = 0 + rpcIsExposed = false + } + continue + } + + serviceDepth += braceDelta(line) + if serviceDepth <= 0 { + serviceName = "" + serviceDepth = 0 + } + continue + } + + rpcIsExposed = rpcIsExposed || exposedTagRE.MatchString(line) + rpcDepth += braceDelta(line) + if rpcDepth <= 0 { + if rpcIsExposed { + rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) + } + rpcName = "" + rpcDepth = 0 + rpcIsExposed = false + } + } + } + return rpcs, nil +} + +func qualifiedRPCName(path string, packageName string, serviceName string, rpcName string) string { + if packageName == "" || serviceName == "" { + fmt.Fprintf(os.Stderr, "%s: exposed RPC has no package or service\n", path) + os.Exit(1) + } + return packageName + "." + serviceName + "." + rpcName +} + +func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { + args := []string{ + "add-rpc", + "--descriptors", descriptors, + "--rpc", rpc, + "--output", output, + } + if input != "" { + args = append(args, "--input", input) + } + + command := exec.Command(nexusAPIGen, args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) + } + return nil +} + +func copyFile(source string, destination string) error { + content, err := os.ReadFile(source) + if err != nil { + return err + } + return os.WriteFile(destination, content, 0o644) +} + +func stripLineComment(line string) string { + if before, _, ok := strings.Cut(line, "//"); ok { + return before + } + return line +} + +func braceDelta(line string) int { + return strings.Count(line, "{") - strings.Count(line, "}") +} diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit new file mode 100644 index 000000000..aa7e12fce --- /dev/null +++ b/nexus/temporal-system.wit @@ -0,0 +1,77 @@ +package temporal:nexus@1.0.0; + +world system { + export workflow-service; +} + +/// @nexus.endpoint "temporal-system" +interface workflow-service { + use nexus:temporal-types/model@1.0.0.{ + duration, + memo, + payloads, + placeholder, + priority, + retry-policy, + search-attributes, + signal-function, + task-queue, + user-metadata, + versioning-override, + workflow-function, + workflow-id-conflict-policy, + workflow-id-reuse-policy, + }; + + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" + record signal-with-start-workflow-execution-request { + /// @nexus.proto-field "workflow_type" + workflow: workflow-function, + workflow-id: string, + task-queue: task-queue, + /// @nexus.proto-field "signal_name" + signal: signal-function, + workflow-execution-timeout: option, + workflow-run-timeout: option, + workflow-task-timeout: option, + identity: option, + request-id: option, + workflow-id-reuse-policy: option, + workflow-id-conflict-policy: option, + retry-policy: option, + cron-schedule: option, + memo: option, + search-attributes: option, + priority: option, + versioning-override: option, + workflow-start-delay: option, + user-metadata: option, + /// @nexus.source python="workflow_namespace" typescript="workflowNamespace" + namespace: string, + /// @nexus.omit + control: placeholder, + /// @nexus.omit + header: placeholder, + /// @nexus.omit + links: placeholder, + /// @nexus.omit + time-skipping-config: placeholder, + } + + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" + record signal-with-start-workflow-execution-response { + run-id: option, + started: option, + /// @nexus.omit + signal-link: placeholder, + } + + /// @nexus.output-transform + /// python-type="workflow.ExternalWorkflowHandle[typing.Any]" + /// python="workflow.get_external_workflow_handle(request.workflow_id, run_id=result.run_id)" + /// typescript-type="workflow.ExternalWorkflowHandle" + /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" + signal-with-start-workflow-execution: func( + request: signal-with-start-workflow-execution-request, + ) -> signal-with-start-workflow-execution-response; +} From c2d52d06d234622f03193c3aa2bf82f520a943f4 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 12:57:46 -0700 Subject: [PATCH 02/14] Expose system Nexus operation metadata --- README.md | 1 + nexus/temporal-system.wit | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 788613ae8..b31d2f70d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Install as git submodule to the project. ## Contribution Make your change to the temporal/proto files, and run `make` to update the openapi definitions. +Rust is also required because `make` installs and runs `nexus-api-gen` when regenerating system Nexus WIT files. ## Breaking changes diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index aa7e12fce..1acb3f7e0 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -71,7 +71,8 @@ interface workflow-service { /// python="workflow.get_external_workflow_handle(request.workflow_id, run_id=result.run_id)" /// typescript-type="workflow.ExternalWorkflowHandle" /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" - signal-with-start-workflow-execution: func( + /// @nexus.operation name="SignalWithStartWorkflowExecution" + signal-with-start-workflow: func( request: signal-with-start-workflow-execution-request, ) -> signal-with-start-workflow-execution-response; } From 5229707452c2cf426eb530bc15878fd3b0581f02 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:13:25 -0700 Subject: [PATCH 03/14] Generate system Nexus WIT with protoc plugin --- Makefile | 41 +-- cmd/gen-system-nexus-wit/main.go | 198 -------------- cmd/protoc-gen-nexus-rpc-yaml/generator.go | 272 ------------------- cmd/protoc-gen-nexus-rpc-yaml/go.mod | 12 - cmd/protoc-gen-nexus-rpc-yaml/go.sum | 10 - cmd/protoc-gen-nexus-rpc-yaml/main.go | 13 - cmd/protoc-gen-system-nexus-wit/generator.go | 168 ++++++++++++ cmd/protoc-gen-system-nexus-wit/go.mod | 8 + cmd/protoc-gen-system-nexus-wit/go.sum | 8 + cmd/protoc-gen-system-nexus-wit/main.go | 11 + nexus/temporal-proto-models-nexusrpc.yaml | 19 -- nexus/temporal-system.wit | 2 +- 12 files changed, 206 insertions(+), 556 deletions(-) delete mode 100644 cmd/gen-system-nexus-wit/main.go delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/generator.go delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.mod delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.sum delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/main.go create mode 100644 cmd/protoc-gen-system-nexus-wit/generator.go create mode 100644 cmd/protoc-gen-system-nexus-wit/go.mod create mode 100644 cmd/protoc-gen-system-nexus-wit/go.sum create mode 100644 cmd/protoc-gen-system-nexus-wit/main.go delete mode 100644 nexus/temporal-proto-models-nexusrpc.yaml diff --git a/Makefile b/Makefile index 9044d1726..f98caab7d 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs install: grpc-install api-linter-install buf-install # Run all linters and compile proto files. -proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit +proto: grpc http-api-docs system-nexus-wit ######################################################################## ##### Variables ###### @@ -37,7 +37,6 @@ OAPI3_PATH := .components.schemas.Payload SYSTEM_NEXUS_OUT := nexus SYSTEM_NEXUS_WIT := $(SYSTEM_NEXUS_OUT)/temporal-system.wit -SYSTEM_NEXUS_DESCRIPTOR := $(PROTO_OUT)/system-nexus/temporal_api.bin SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) GO_BUILD_CACHE ?= $(abspath $(PROTO_OUT)/go-build-cache) NEXUS_API_GEN ?= nexus-api-gen @@ -130,41 +129,21 @@ buf-breaking: @printf $(COLOR) "Run buf breaking changes check against master branch..." @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') -nexus-rpc-yaml: nexus-rpc-yaml-install - printf $(COLOR) "Generate nexus/temporal-proto-models-nexusrpc.yaml..." - mkdir -p nexus - protoc -I $(PROTO_ROOT) \ - --nexus-rpc-yaml_opt=nexus-rpc_langs_out=nexus/temporal-proto-models-nexusrpc.yaml \ - --nexus-rpc-yaml_opt=python_package_prefix=temporalio.api \ - --nexus-rpc-yaml_opt=typescript_package_prefix=@temporalio/api \ - --nexus-rpc-yaml_opt=include_operation_tags=exposed \ - --nexus-rpc-yaml_out=. \ - temporal/api/workflowservice/v1/* \ - temporal/api/operatorservice/v1/* - -nexus-rpc-yaml-install: - printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..." - @cd cmd/protoc-gen-nexus-rpc-yaml && go install . - ##### Compile system Nexus WIT files ##### system-nexus-wit: $(SYSTEM_NEXUS_WIT) -$(SYSTEM_NEXUS_DESCRIPTOR): $(PROTO_FILES) | $(PROTO_OUT) - printf $(COLOR) "Build Temporal API descriptor set for system Nexus WIT..." - mkdir -p $(@D) +$(SYSTEM_NEXUS_WIT): $(PROTO_FILES) cmd/protoc-gen-system-nexus-wit/main.go cmd/protoc-gen-system-nexus-wit/generator.go | system-nexus-wit-install $(STAMPDIR)/nexus-api-gen-install + printf $(COLOR) "Generate system Nexus WIT..." + mkdir -p $(SYSTEM_NEXUS_OUT) protoc -I $(PROTO_ROOT) \ - --include_imports \ - --include_source_info \ - --descriptor_set_out=$@ \ + --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ + --system-nexus-wit_opt=nexus_api_gen=$(NEXUS_API_GEN) \ + --system-nexus-wit_out=. \ $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) -$(SYSTEM_NEXUS_WIT): $(SYSTEM_NEXUS_DESCRIPTOR) cmd/gen-system-nexus-wit/main.go $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) | $(STAMPDIR)/nexus-api-gen-install - printf $(COLOR) "Generate system Nexus WIT..." - GOCACHE=$(GO_BUILD_CACHE) go run cmd/gen-system-nexus-wit/main.go \ - --descriptors $(SYSTEM_NEXUS_DESCRIPTOR) \ - --nexus-api-gen $(NEXUS_API_GEN) \ - --output $@ \ - $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) +system-nexus-wit-install: | $(PROTO_OUT) + printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..." + @cd cmd/protoc-gen-system-nexus-wit && GOCACHE=$(GO_BUILD_CACHE) go install . $(STAMPDIR)/nexus-api-gen-install: | $(STAMPDIR) printf $(COLOR) "Install nexus-api-gen if missing..." diff --git a/cmd/gen-system-nexus-wit/main.go b/cmd/gen-system-nexus-wit/main.go deleted file mode 100644 index 865b5f67b..000000000 --- a/cmd/gen-system-nexus-wit/main.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" -) - -var ( - exposedTagRE = regexp.MustCompile(`\btags\s*=\s*"exposed"`) - packageRE = regexp.MustCompile(`^\s*package\s+([A-Za-z0-9_.]+)\s*;`) - rpcRE = regexp.MustCompile(`^\s*rpc\s+([A-Za-z0-9_]+)\s*\(`) - serviceRE = regexp.MustCompile(`^\s*service\s+([A-Za-z0-9_]+)\s*\{`) -) - -func main() { - var descriptors string - var nexusAPIGen string - var output string - flag.StringVar(&descriptors, "descriptors", "", "protobuf descriptor set") - flag.StringVar(&nexusAPIGen, "nexus-api-gen", "nexus-api-gen", "path to nexus-api-gen") - flag.StringVar(&output, "output", "", "output WIT file") - flag.Parse() - - if descriptors == "" || output == "" || flag.NArg() == 0 { - fmt.Fprintf(os.Stderr, "usage: gen_system_nexus_wit --descriptors DESCRIPTORS [--nexus-api-gen PATH] --output OUTPUT PROTO...\n") - os.Exit(2) - } - - rpcs, err := exposedRPCs(flag.Args()) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - if len(rpcs) == 0 { - fmt.Fprintln(os.Stderr, "no proto RPCs are marked as exposed Nexus operations") - os.Exit(1) - } - - tempDir, err := os.MkdirTemp("", "system-nexus-wit-*") - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - defer os.RemoveAll(tempDir) - - tempOutput := filepath.Join(tempDir, "system-nexus.wit") - var input string - if _, err := os.Stat(output); err == nil { - if err := copyFile(output, tempOutput); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - input = tempOutput - } else if !os.IsNotExist(err) { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - for _, rpc := range rpcs { - if err := runAddRPC(nexusAPIGen, descriptors, rpc, tempOutput, input); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - input = tempOutput - } - - if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - if err := copyFile(tempOutput, output); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func exposedRPCs(paths []string) ([]string, error) { - var rpcs []string - for _, path := range paths { - content, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var packageName string - var serviceName string - var serviceDepth int - var rpcName string - var rpcDepth int - var rpcIsExposed bool - - for _, rawLine := range strings.Split(string(content), "\n") { - line := stripLineComment(rawLine) - - if packageName == "" { - if match := packageRE.FindStringSubmatch(line); match != nil { - packageName = match[1] - } - } - - if serviceName == "" { - if match := serviceRE.FindStringSubmatch(line); match != nil { - serviceName = match[1] - serviceDepth = braceDelta(line) - } - continue - } - - if rpcName == "" { - if match := rpcRE.FindStringSubmatch(line); match != nil { - rpcName = match[1] - rpcDepth = braceDelta(line) - rpcIsExposed = exposedTagRE.MatchString(line) - if rpcDepth <= 0 { - if rpcIsExposed { - rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) - } - rpcName = "" - rpcDepth = 0 - rpcIsExposed = false - } - continue - } - - serviceDepth += braceDelta(line) - if serviceDepth <= 0 { - serviceName = "" - serviceDepth = 0 - } - continue - } - - rpcIsExposed = rpcIsExposed || exposedTagRE.MatchString(line) - rpcDepth += braceDelta(line) - if rpcDepth <= 0 { - if rpcIsExposed { - rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) - } - rpcName = "" - rpcDepth = 0 - rpcIsExposed = false - } - } - } - return rpcs, nil -} - -func qualifiedRPCName(path string, packageName string, serviceName string, rpcName string) string { - if packageName == "" || serviceName == "" { - fmt.Fprintf(os.Stderr, "%s: exposed RPC has no package or service\n", path) - os.Exit(1) - } - return packageName + "." + serviceName + "." + rpcName -} - -func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { - args := []string{ - "add-rpc", - "--descriptors", descriptors, - "--rpc", rpc, - "--output", output, - } - if input != "" { - args = append(args, "--input", input) - } - - command := exec.Command(nexusAPIGen, args...) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - if err := command.Run(); err != nil { - return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) - } - return nil -} - -func copyFile(source string, destination string) error { - content, err := os.ReadFile(source) - if err != nil { - return err - } - return os.WriteFile(destination, content, 0o644) -} - -func stripLineComment(line string) string { - if before, _, ok := strings.Cut(line, "//"); ok { - return before - } - return line -} - -func braceDelta(line string) int { - return strings.Count(line, "{") - strings.Count(line, "}") -} diff --git a/cmd/protoc-gen-nexus-rpc-yaml/generator.go b/cmd/protoc-gen-nexus-rpc-yaml/generator.go deleted file mode 100644 index 2e4a939c8..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/generator.go +++ /dev/null @@ -1,272 +0,0 @@ -package main - -import ( - "fmt" - "slices" - "sort" - "strings" - - nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" - "google.golang.org/protobuf/compiler/protogen" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" - "gopkg.in/yaml.v3" -) - -// params holds the parsed protoc plugin options. -// Passed via --nexus-rpc-yaml_opt=key=value (multiple opts are comma-joined by protoc). -// -// - nexus-rpc_langs_out: optional. Output path for the langs YAML. -// If empty, nothing is written. -// Example: "nexus/temporal-proto-models-nexusrpc.yaml" -// -// - python_package_prefix: optional. Dot-separated package prefix for $pythonRef. -// The last two path segments of the go_package ({service}/v{n}) are appended. -// Example: "temporalio.api" → "temporalio.api.workflowservice.v1.TypeName" -// If empty, $pythonRef is omitted. -// -// - typescript_package_prefix: optional. Scoped package prefix for $typescriptRef. -// The last two path segments of the go_package ({service}/v{n}) are appended. -// Example: "@temporalio/api" → "@temporalio/api/workflowservice/v1.TypeName" -// If empty, $typescriptRef is omitted. -// -// - include_operation_tags: optional, repeatable. Only include operations whose tags -// contain at least one of these values. If empty, all annotated operations are included -// (subject to exclude_operation_tags). Specify multiple times for multiple tags. -// Example: include_operation_tags=exposed -// -// - exclude_operation_tags: optional, repeatable. Exclude operations whose tags contain -// any of these values. Applied after include_operation_tags. -// Example: exclude_operation_tags=internal -type params struct { - nexusRpcLangsOut string - pythonPackagePrefix string - typescriptPackagePrefix string - includeOperationTags []string - excludeOperationTags []string -} - -// parseParams parses the comma-separated key=value parameter string provided by protoc. -func parseParams(raw string) (params, error) { - var p params - if raw == "" { - return p, nil - } - for kv := range strings.SplitSeq(raw, ",") { - key, value, ok := strings.Cut(kv, "=") - if !ok { - return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) - } - switch key { - case "nexus-rpc_langs_out": - p.nexusRpcLangsOut = value - case "python_package_prefix": - p.pythonPackagePrefix = value - case "typescript_package_prefix": - p.typescriptPackagePrefix = value - case "include_operation_tags": - p.includeOperationTags = append(p.includeOperationTags, value) - case "exclude_operation_tags": - p.excludeOperationTags = append(p.excludeOperationTags, value) - default: - return p, fmt.Errorf("unknown parameter %q", key) - } - } - return p, nil -} - -// shouldIncludeOperation returns true if the method's nexus operation tags pass -// the include/exclude filters. Mirrors the logic from protoc-gen-go-nexus: -// 1. Method must have the nexus operation extension set. -// 2. If includeOperationTags is non-empty, at least one of the method's tags must match. -// 3. If excludeOperationTags is non-empty, none of the method's tags may match. -func shouldIncludeOperation(p params, m *protogen.Method) bool { - opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions) - if !ok || opts == nil { - return false - } - if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) { - return false - } - tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags() - if len(p.includeOperationTags) > 0 && !slices.ContainsFunc(p.includeOperationTags, func(t string) bool { - return slices.Contains(tags, t) - }) { - return false - } - return !slices.ContainsFunc(p.excludeOperationTags, func(t string) bool { - return slices.Contains(tags, t) - }) -} - -func generate(gen *protogen.Plugin) error { - p, err := parseParams(gen.Request.GetParameter()) - if err != nil { - return err - } - - langsDoc := newDoc() - hasOps := false - - for _, f := range gen.Files { - if !f.Generate { - continue - } - for _, svc := range f.Services { - for _, m := range svc.Methods { - if !shouldIncludeOperation(p, m) { - continue - } - svcName := string(svc.Desc.Name()) - methodName := string(m.Desc.Name()) - hasOps = true - addOperation(langsDoc, svcName, methodName, - langRefs(p, f.Desc, m.Input.Desc), - langRefs(p, f.Desc, m.Output.Desc), - ) - } - } - } - - if !hasOps { - return nil - } - if p.nexusRpcLangsOut != "" { - return writeFile(gen, p.nexusRpcLangsOut, langsDoc) - } - return nil -} - -// langRefs builds the map of language-specific type refs for a message. -// -// Go, Java, dotnet, and Ruby refs are derived from proto file-level package options. -// Python and TypeScript refs require the corresponding prefix params to be set; if -// empty they are omitted. Both use the last two path segments of go_package -// ({service}/v{n}), dropping any intermediate grouping directory. -func langRefs(p params, file protoreflect.FileDescriptor, msg protoreflect.MessageDescriptor) map[string]string { - opts, ok := file.Options().(*descriptorpb.FileOptions) - if !ok || opts == nil { - return nil - } - name := string(msg.Name()) - refs := make(map[string]string) - - if pkg := opts.GetGoPackage(); pkg != "" { - // strip the ";alias" suffix (e.g. "go.temporal.io/api/workflowservice/v1;workflowservice") - pkg = strings.SplitN(pkg, ";", 2)[0] - refs["$goRef"] = pkg + "." + name - - segments := strings.Split(pkg, "/") - if len(segments) >= 2 { - tail := segments[len(segments)-2] + "/" + segments[len(segments)-1] - if p.pythonPackagePrefix != "" { - dotTail := strings.ReplaceAll(tail, "/", ".") - refs["$pythonRef"] = p.pythonPackagePrefix + "." + dotTail + "." + name - } - if p.typescriptPackagePrefix != "" { - refs["$typescriptRef"] = p.typescriptPackagePrefix + "/" + tail + "." + name - } - } - } - if pkg := opts.GetJavaPackage(); pkg != "" { - refs["$javaRef"] = pkg + "." + name - } - if pkg := opts.GetRubyPackage(); pkg != "" { - refs["$rubyRef"] = pkg + "::" + name - } - if pkg := opts.GetCsharpNamespace(); pkg != "" { - refs["$dotnetRef"] = pkg + "." + name - } - if len(refs) == 0 { - return nil - } - return refs -} - -// newDoc creates a yaml.Node document with the "nexusrpc: 1.0.0" header -// and an empty "services" mapping node. -func newDoc() *yaml.Node { - doc := &yaml.Node{Kind: yaml.DocumentNode} - root := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - doc.Content = []*yaml.Node{root} - root.Content = append(root.Content, - scalarNode("nexusrpc"), - scalarNode("1.0.0"), - scalarNode("services"), - &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, - ) - return doc -} - -// servicesNode returns the "services" mapping node from a doc created by newDoc. -func servicesNode(doc *yaml.Node) *yaml.Node { - root := doc.Content[0] - for i := 0; i < len(root.Content)-1; i += 2 { - if root.Content[i].Value == "services" { - return root.Content[i+1] - } - } - panic("services node not found") -} - -// addOperation inserts a service → operation → {input, output} entry into doc. -// Services and operations are inserted in the order first encountered. -func addOperation(doc *yaml.Node, svcName, methodName string, input, output map[string]string) { - svcs := servicesNode(doc) - - var svcOps *yaml.Node - for i := 0; i < len(svcs.Content)-1; i += 2 { - if svcs.Content[i].Value == svcName { - svcMap := svcs.Content[i+1] - for j := 0; j < len(svcMap.Content)-1; j += 2 { - if svcMap.Content[j].Value == "operations" { - svcOps = svcMap.Content[j+1] - } - } - } - } - if svcOps == nil { - svcMap := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - svcOps = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - svcMap.Content = append(svcMap.Content, scalarNode("operations"), svcOps) - svcs.Content = append(svcs.Content, scalarNode(svcName), svcMap) - } - - opNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - if len(input) > 0 { - opNode.Content = append(opNode.Content, scalarNode("input"), mapNode(input)) - } - if len(output) > 0 { - opNode.Content = append(opNode.Content, scalarNode("output"), mapNode(output)) - } - svcOps.Content = append(svcOps.Content, scalarNode(methodName), opNode) -} - -// mapNode serializes a map[string]string as a yaml mapping node with keys in sorted order. -func mapNode(m map[string]string) *yaml.Node { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - for _, k := range keys { - node.Content = append(node.Content, scalarNode(k), scalarNode(m[k])) - } - return node -} - -func scalarNode(value string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value} -} - -func writeFile(gen *protogen.Plugin, name string, doc *yaml.Node) error { - f := gen.NewGeneratedFile(name, "") - enc := yaml.NewEncoder(f) - enc.SetIndent(2) - if err := enc.Encode(doc); err != nil { - return err - } - return enc.Close() -} diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.mod b/cmd/protoc-gen-nexus-rpc-yaml/go.mod deleted file mode 100644 index 863c771a6..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module github.com/temporalio/api/cmd/protoc-gen-nexus-rpc-yaml - -go 1.25.4 - -require ( - google.golang.org/protobuf v1.36.1 - gopkg.in/yaml.v3 v3.0.1 -) - -require github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 - -require github.com/google/go-cmp v0.6.0 // indirect diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.sum b/cmd/protoc-gen-nexus-rpc-yaml/go.sum deleted file mode 100644 index cbc5252ff..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/go.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY= -github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/protoc-gen-nexus-rpc-yaml/main.go b/cmd/protoc-gen-nexus-rpc-yaml/main.go deleted file mode 100644 index 31cdca92f..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/main.go +++ /dev/null @@ -1,13 +0,0 @@ -// protoc-gen-nexus-rpc-yaml is a protoc plugin that generates nexus/temporal-proto-models-nexusrpc.yaml -// from proto service methods annotated with option (nexusannotations.v1.operation).tags = "exposed". -package main - -import ( - "google.golang.org/protobuf/compiler/protogen" -) - -func main() { - protogen.Options{}.Run(func(gen *protogen.Plugin) error { - return generate(gen) - }) -} diff --git a/cmd/protoc-gen-system-nexus-wit/generator.go b/cmd/protoc-gen-system-nexus-wit/generator.go new file mode 100644 index 000000000..0e7207266 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/generator.go @@ -0,0 +1,168 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +type params struct { + nexusAPIGen string + output string + input string +} + +func parseParams(raw string) (params, error) { + p := params{ + nexusAPIGen: "nexus-api-gen", + } + if raw == "" { + return p, nil + } + for kv := range strings.SplitSeq(raw, ",") { + key, value, ok := strings.Cut(kv, "=") + if !ok { + return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) + } + switch key { + case "nexus_api_gen": + p.nexusAPIGen = value + case "output": + p.output = value + case "input": + p.input = value + default: + return p, fmt.Errorf("unknown parameter %q", key) + } + } + return p, nil +} + +func generate(gen *protogen.Plugin) error { + p, err := parseParams(gen.Request.GetParameter()) + if err != nil { + return err + } + if p.output == "" { + return fmt.Errorf("missing required output parameter") + } + if p.input == "" { + p.input = p.output + } + + rpcs := exposedRPCs(gen) + if len(rpcs) == 0 { + return fmt.Errorf("no proto RPCs are marked as exposed Nexus operations") + } + + tempDir, err := os.MkdirTemp("", "system-nexus-wit-*") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + descriptorPath := filepath.Join(tempDir, "temporal_api.bin") + if err := writeDescriptorSet(gen, descriptorPath); err != nil { + return err + } + + tempOutput := filepath.Join(tempDir, "system-nexus.wit") + input := "" + if _, err := os.Stat(p.input); err == nil { + if err := copyFile(p.input, tempOutput); err != nil { + return err + } + input = tempOutput + } else if !os.IsNotExist(err) { + return err + } + + for _, rpc := range rpcs { + if err := runAddRPC(p.nexusAPIGen, descriptorPath, rpc, tempOutput, input); err != nil { + return err + } + input = tempOutput + } + + content, err := os.ReadFile(tempOutput) + if err != nil { + return err + } + _, err = gen.NewGeneratedFile(p.output, "").Write(content) + return err +} + +func exposedRPCs(gen *protogen.Plugin) []string { + var rpcs []string + for _, f := range gen.Files { + if !f.Generate { + continue + } + for _, svc := range f.Services { + for _, m := range svc.Methods { + if isExposedOperation(m) { + rpcs = append(rpcs, string(m.Desc.FullName())) + } + } + } + } + return rpcs +} + +func isExposedOperation(m *protogen.Method) bool { + opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions) + if !ok || opts == nil { + return false + } + if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) { + return false + } + tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags() + return slices.Contains(tags, "exposed") +} + +func writeDescriptorSet(gen *protogen.Plugin, descriptorPath string) error { + data, err := proto.Marshal(&descriptorpb.FileDescriptorSet{ + File: gen.Request.GetProtoFile(), + }) + if err != nil { + return err + } + return os.WriteFile(descriptorPath, data, 0o644) +} + +func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { + args := []string{ + "add-rpc", + "--descriptors", descriptors, + "--rpc", rpc, + "--output", output, + } + if input != "" { + args = append(args, "--input", input) + } + + command := exec.Command(nexusAPIGen, args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) + } + return nil +} + +func copyFile(source string, destination string) error { + content, err := os.ReadFile(source) + if err != nil { + return err + } + return os.WriteFile(destination, content, 0o644) +} diff --git a/cmd/protoc-gen-system-nexus-wit/go.mod b/cmd/protoc-gen-system-nexus-wit/go.mod new file mode 100644 index 000000000..222e32187 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/go.mod @@ -0,0 +1,8 @@ +module github.com/temporalio/api/cmd/protoc-gen-system-nexus-wit + +go 1.25.4 + +require ( + github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 + google.golang.org/protobuf v1.36.1 +) diff --git a/cmd/protoc-gen-system-nexus-wit/go.sum b/cmd/protoc-gen-system-nexus-wit/go.sum new file mode 100644 index 000000000..c96f6a1c5 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/go.sum @@ -0,0 +1,8 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/cmd/protoc-gen-system-nexus-wit/main.go b/cmd/protoc-gen-system-nexus-wit/main.go new file mode 100644 index 000000000..0e9fe3182 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/main.go @@ -0,0 +1,11 @@ +// protoc-gen-system-nexus-wit generates nexus/temporal-system.wit from proto service +// methods annotated with option (nexusannotations.v1.operation).tags = "exposed". +package main + +import "google.golang.org/protobuf/compiler/protogen" + +func main() { + protogen.Options{}.Run(func(gen *protogen.Plugin) error { + return generate(gen) + }) +} diff --git a/nexus/temporal-proto-models-nexusrpc.yaml b/nexus/temporal-proto-models-nexusrpc.yaml deleted file mode 100644 index e0761fd15..000000000 --- a/nexus/temporal-proto-models-nexusrpc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -nexusrpc: 1.0.0 -services: - WorkflowService: - operations: - SignalWithStartWorkflowExecution: - input: - $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest - $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest - $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest - $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest' - output: - $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse - $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse - $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse - $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse - $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse - $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse' diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index 1acb3f7e0..29021b42a 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -4,7 +4,7 @@ world system { export workflow-service; } -/// @nexus.endpoint "temporal-system" +/// @nexus.endpoint "__temporal_system" interface workflow-service { use nexus:temporal-types/model@1.0.0.{ duration, From bdb13b28a2238e58f7aa2b4c1dff17d5e45840e9 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:21:05 -0700 Subject: [PATCH 04/14] Simplify system Nexus WIT make target --- Makefile | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index f98caab7d..4f06eaf12 100644 --- a/Makefile +++ b/Makefile @@ -35,14 +35,12 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT) OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload -SYSTEM_NEXUS_OUT := nexus -SYSTEM_NEXUS_WIT := $(SYSTEM_NEXUS_OUT)/temporal-system.wit +SYSTEM_NEXUS_WIT := nexus/temporal-system.wit SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) -GO_BUILD_CACHE ?= $(abspath $(PROTO_OUT)/go-build-cache) NEXUS_API_GEN ?= nexus-api-gen $(PROTO_OUT): - mkdir -p $(PROTO_OUT) + mkdir $(PROTO_OUT) ##### Compile proto files for go ##### grpc: buf-lint api-linter buf-breaking clean go-grpc fix-path @@ -114,7 +112,7 @@ api-linter: @api-linter --set-exit-status $(PROTO_IMPORTS) --config $(PROTO_ROOT)/api-linter.yaml --output-format json $(PROTO_FILES) | gojq -r 'map(select(.problems != []) | . as $$file | .problems[] | {rule: .rule_doc_uri, location: "\($$file.file_path):\(.location.start_position.line_number)"}) | group_by(.rule) | .[] | .[0].rule + ":\n" + (map("\t" + .location) | join("\n"))' $(STAMPDIR): - mkdir -p $@ + mkdir $@ $(STAMPDIR)/buf-mod-prune: $(STAMPDIR) buf.yaml printf $(COLOR) "Pruning buf module" @@ -130,27 +128,21 @@ buf-breaking: @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') ##### Compile system Nexus WIT files ##### -system-nexus-wit: $(SYSTEM_NEXUS_WIT) - -$(SYSTEM_NEXUS_WIT): $(PROTO_FILES) cmd/protoc-gen-system-nexus-wit/main.go cmd/protoc-gen-system-nexus-wit/generator.go | system-nexus-wit-install $(STAMPDIR)/nexus-api-gen-install +system-nexus-wit: system-nexus-wit-install nexus-api-gen-install printf $(COLOR) "Generate system Nexus WIT..." - mkdir -p $(SYSTEM_NEXUS_OUT) protoc -I $(PROTO_ROOT) \ --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ --system-nexus-wit_opt=nexus_api_gen=$(NEXUS_API_GEN) \ --system-nexus-wit_out=. \ $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) -system-nexus-wit-install: | $(PROTO_OUT) +system-nexus-wit-install: printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..." - @cd cmd/protoc-gen-system-nexus-wit && GOCACHE=$(GO_BUILD_CACHE) go install . + @cd cmd/protoc-gen-system-nexus-wit && go install . -$(STAMPDIR)/nexus-api-gen-install: | $(STAMPDIR) +nexus-api-gen-install: printf $(COLOR) "Install nexus-api-gen if missing..." command -v $(NEXUS_API_GEN) >/dev/null || CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/temporalio/nexus-api-gen - touch $@ - -nexus-api-gen-install: $(STAMPDIR)/nexus-api-gen-install ##### Clean ##### clean: From 2dd27ca64b1f4febfeec361bc579cb8841666469 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:28:01 -0700 Subject: [PATCH 05/14] Document system Nexus WIT plugin parameters --- cmd/protoc-gen-system-nexus-wit/generator.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/protoc-gen-system-nexus-wit/generator.go b/cmd/protoc-gen-system-nexus-wit/generator.go index 0e7207266..31fdfbb43 100644 --- a/cmd/protoc-gen-system-nexus-wit/generator.go +++ b/cmd/protoc-gen-system-nexus-wit/generator.go @@ -20,6 +20,17 @@ type params struct { input string } +// parseParams parses the comma-separated key=value parameter string provided by protoc. +// +// - output: required. Path to the WIT file to generate, relative to the +// --system-nexus-wit_out directory. Example: "nexus/temporal-system.wit". +// +// - nexus_api_gen: optional. Path to the nexus-api-gen binary. Defaults to +// "nexus-api-gen". +// +// - input: optional. Existing WIT file to update. Defaults to output, so +// existing handwritten annotations and type refinements are preserved when +// regenerating in place. func parseParams(raw string) (params, error) { p := params{ nexusAPIGen: "nexus-api-gen", From ffa051e452976eeba3704481d1ee2b872df05968 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:50:26 -0700 Subject: [PATCH 06/14] Shorten signal-with-start WIT names --- nexus/temporal-system.wit | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index 29021b42a..e410c64d7 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -24,7 +24,7 @@ interface workflow-service { }; /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" - record signal-with-start-workflow-execution-request { + record signal-with-start-workflow-request { /// @nexus.proto-field "workflow_type" workflow: workflow-function, workflow-id: string, @@ -59,7 +59,7 @@ interface workflow-service { } /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" - record signal-with-start-workflow-execution-response { + record signal-with-start-workflow-response { run-id: option, started: option, /// @nexus.omit @@ -73,6 +73,6 @@ interface workflow-service { /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" /// @nexus.operation name="SignalWithStartWorkflowExecution" signal-with-start-workflow: func( - request: signal-with-start-workflow-execution-request, - ) -> signal-with-start-workflow-execution-response; + request: signal-with-start-workflow-request, + ) -> signal-with-start-workflow-response; } From abd072dc213e05ced42774dd806f01fab298fec9 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Tue, 26 May 2026 08:13:01 -0700 Subject: [PATCH 07/14] Fix service name --- nexus/temporal-system.wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index aa7e12fce..d17c76fc8 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -4,7 +4,7 @@ world system { export workflow-service; } -/// @nexus.endpoint "temporal-system" +/// @nexus.endpoint "__temporal_system" interface workflow-service { use nexus:temporal-types/model@1.0.0.{ duration, From 47a5e622d5981a7e0a2ecbf7f1444dcde11137f5 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Tue, 26 May 2026 08:17:53 -0700 Subject: [PATCH 08/14] Restore Nexus RPC YAML generation --- Makefile | 18 +- cmd/protoc-gen-nexus-rpc-yaml/generator.go | 272 +++++++++++++++++++++ cmd/protoc-gen-nexus-rpc-yaml/go.mod | 12 + cmd/protoc-gen-nexus-rpc-yaml/go.sum | 10 + cmd/protoc-gen-nexus-rpc-yaml/main.go | 13 + nexus/temporal-proto-models-nexusrpc.yaml | 19 ++ 6 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/generator.go create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.mod create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.sum create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/main.go create mode 100644 nexus/temporal-proto-models-nexusrpc.yaml diff --git a/Makefile b/Makefile index 4f06eaf12..8d230e741 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs install: grpc-install api-linter-install buf-install # Run all linters and compile proto files. -proto: grpc http-api-docs system-nexus-wit +proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit ######################################################################## ##### Variables ###### @@ -127,6 +127,22 @@ buf-breaking: @printf $(COLOR) "Run buf breaking changes check against master branch..." @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') +nexus-rpc-yaml: nexus-rpc-yaml-install + printf $(COLOR) "Generate nexus/temporal-proto-models-nexusrpc.yaml..." + mkdir -p nexus + protoc -I $(PROTO_ROOT) \ + --nexus-rpc-yaml_opt=nexus-rpc_langs_out=nexus/temporal-proto-models-nexusrpc.yaml \ + --nexus-rpc-yaml_opt=python_package_prefix=temporalio.api \ + --nexus-rpc-yaml_opt=typescript_package_prefix=@temporalio/api \ + --nexus-rpc-yaml_opt=include_operation_tags=exposed \ + --nexus-rpc-yaml_out=. \ + temporal/api/workflowservice/v1/* \ + temporal/api/operatorservice/v1/* + +nexus-rpc-yaml-install: + printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..." + @cd cmd/protoc-gen-nexus-rpc-yaml && go install . + ##### Compile system Nexus WIT files ##### system-nexus-wit: system-nexus-wit-install nexus-api-gen-install printf $(COLOR) "Generate system Nexus WIT..." diff --git a/cmd/protoc-gen-nexus-rpc-yaml/generator.go b/cmd/protoc-gen-nexus-rpc-yaml/generator.go new file mode 100644 index 000000000..2e4a939c8 --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/generator.go @@ -0,0 +1,272 @@ +package main + +import ( + "fmt" + "slices" + "sort" + "strings" + + nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "gopkg.in/yaml.v3" +) + +// params holds the parsed protoc plugin options. +// Passed via --nexus-rpc-yaml_opt=key=value (multiple opts are comma-joined by protoc). +// +// - nexus-rpc_langs_out: optional. Output path for the langs YAML. +// If empty, nothing is written. +// Example: "nexus/temporal-proto-models-nexusrpc.yaml" +// +// - python_package_prefix: optional. Dot-separated package prefix for $pythonRef. +// The last two path segments of the go_package ({service}/v{n}) are appended. +// Example: "temporalio.api" → "temporalio.api.workflowservice.v1.TypeName" +// If empty, $pythonRef is omitted. +// +// - typescript_package_prefix: optional. Scoped package prefix for $typescriptRef. +// The last two path segments of the go_package ({service}/v{n}) are appended. +// Example: "@temporalio/api" → "@temporalio/api/workflowservice/v1.TypeName" +// If empty, $typescriptRef is omitted. +// +// - include_operation_tags: optional, repeatable. Only include operations whose tags +// contain at least one of these values. If empty, all annotated operations are included +// (subject to exclude_operation_tags). Specify multiple times for multiple tags. +// Example: include_operation_tags=exposed +// +// - exclude_operation_tags: optional, repeatable. Exclude operations whose tags contain +// any of these values. Applied after include_operation_tags. +// Example: exclude_operation_tags=internal +type params struct { + nexusRpcLangsOut string + pythonPackagePrefix string + typescriptPackagePrefix string + includeOperationTags []string + excludeOperationTags []string +} + +// parseParams parses the comma-separated key=value parameter string provided by protoc. +func parseParams(raw string) (params, error) { + var p params + if raw == "" { + return p, nil + } + for kv := range strings.SplitSeq(raw, ",") { + key, value, ok := strings.Cut(kv, "=") + if !ok { + return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) + } + switch key { + case "nexus-rpc_langs_out": + p.nexusRpcLangsOut = value + case "python_package_prefix": + p.pythonPackagePrefix = value + case "typescript_package_prefix": + p.typescriptPackagePrefix = value + case "include_operation_tags": + p.includeOperationTags = append(p.includeOperationTags, value) + case "exclude_operation_tags": + p.excludeOperationTags = append(p.excludeOperationTags, value) + default: + return p, fmt.Errorf("unknown parameter %q", key) + } + } + return p, nil +} + +// shouldIncludeOperation returns true if the method's nexus operation tags pass +// the include/exclude filters. Mirrors the logic from protoc-gen-go-nexus: +// 1. Method must have the nexus operation extension set. +// 2. If includeOperationTags is non-empty, at least one of the method's tags must match. +// 3. If excludeOperationTags is non-empty, none of the method's tags may match. +func shouldIncludeOperation(p params, m *protogen.Method) bool { + opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions) + if !ok || opts == nil { + return false + } + if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) { + return false + } + tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags() + if len(p.includeOperationTags) > 0 && !slices.ContainsFunc(p.includeOperationTags, func(t string) bool { + return slices.Contains(tags, t) + }) { + return false + } + return !slices.ContainsFunc(p.excludeOperationTags, func(t string) bool { + return slices.Contains(tags, t) + }) +} + +func generate(gen *protogen.Plugin) error { + p, err := parseParams(gen.Request.GetParameter()) + if err != nil { + return err + } + + langsDoc := newDoc() + hasOps := false + + for _, f := range gen.Files { + if !f.Generate { + continue + } + for _, svc := range f.Services { + for _, m := range svc.Methods { + if !shouldIncludeOperation(p, m) { + continue + } + svcName := string(svc.Desc.Name()) + methodName := string(m.Desc.Name()) + hasOps = true + addOperation(langsDoc, svcName, methodName, + langRefs(p, f.Desc, m.Input.Desc), + langRefs(p, f.Desc, m.Output.Desc), + ) + } + } + } + + if !hasOps { + return nil + } + if p.nexusRpcLangsOut != "" { + return writeFile(gen, p.nexusRpcLangsOut, langsDoc) + } + return nil +} + +// langRefs builds the map of language-specific type refs for a message. +// +// Go, Java, dotnet, and Ruby refs are derived from proto file-level package options. +// Python and TypeScript refs require the corresponding prefix params to be set; if +// empty they are omitted. Both use the last two path segments of go_package +// ({service}/v{n}), dropping any intermediate grouping directory. +func langRefs(p params, file protoreflect.FileDescriptor, msg protoreflect.MessageDescriptor) map[string]string { + opts, ok := file.Options().(*descriptorpb.FileOptions) + if !ok || opts == nil { + return nil + } + name := string(msg.Name()) + refs := make(map[string]string) + + if pkg := opts.GetGoPackage(); pkg != "" { + // strip the ";alias" suffix (e.g. "go.temporal.io/api/workflowservice/v1;workflowservice") + pkg = strings.SplitN(pkg, ";", 2)[0] + refs["$goRef"] = pkg + "." + name + + segments := strings.Split(pkg, "/") + if len(segments) >= 2 { + tail := segments[len(segments)-2] + "/" + segments[len(segments)-1] + if p.pythonPackagePrefix != "" { + dotTail := strings.ReplaceAll(tail, "/", ".") + refs["$pythonRef"] = p.pythonPackagePrefix + "." + dotTail + "." + name + } + if p.typescriptPackagePrefix != "" { + refs["$typescriptRef"] = p.typescriptPackagePrefix + "/" + tail + "." + name + } + } + } + if pkg := opts.GetJavaPackage(); pkg != "" { + refs["$javaRef"] = pkg + "." + name + } + if pkg := opts.GetRubyPackage(); pkg != "" { + refs["$rubyRef"] = pkg + "::" + name + } + if pkg := opts.GetCsharpNamespace(); pkg != "" { + refs["$dotnetRef"] = pkg + "." + name + } + if len(refs) == 0 { + return nil + } + return refs +} + +// newDoc creates a yaml.Node document with the "nexusrpc: 1.0.0" header +// and an empty "services" mapping node. +func newDoc() *yaml.Node { + doc := &yaml.Node{Kind: yaml.DocumentNode} + root := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + doc.Content = []*yaml.Node{root} + root.Content = append(root.Content, + scalarNode("nexusrpc"), + scalarNode("1.0.0"), + scalarNode("services"), + &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, + ) + return doc +} + +// servicesNode returns the "services" mapping node from a doc created by newDoc. +func servicesNode(doc *yaml.Node) *yaml.Node { + root := doc.Content[0] + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == "services" { + return root.Content[i+1] + } + } + panic("services node not found") +} + +// addOperation inserts a service → operation → {input, output} entry into doc. +// Services and operations are inserted in the order first encountered. +func addOperation(doc *yaml.Node, svcName, methodName string, input, output map[string]string) { + svcs := servicesNode(doc) + + var svcOps *yaml.Node + for i := 0; i < len(svcs.Content)-1; i += 2 { + if svcs.Content[i].Value == svcName { + svcMap := svcs.Content[i+1] + for j := 0; j < len(svcMap.Content)-1; j += 2 { + if svcMap.Content[j].Value == "operations" { + svcOps = svcMap.Content[j+1] + } + } + } + } + if svcOps == nil { + svcMap := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + svcOps = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + svcMap.Content = append(svcMap.Content, scalarNode("operations"), svcOps) + svcs.Content = append(svcs.Content, scalarNode(svcName), svcMap) + } + + opNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + if len(input) > 0 { + opNode.Content = append(opNode.Content, scalarNode("input"), mapNode(input)) + } + if len(output) > 0 { + opNode.Content = append(opNode.Content, scalarNode("output"), mapNode(output)) + } + svcOps.Content = append(svcOps.Content, scalarNode(methodName), opNode) +} + +// mapNode serializes a map[string]string as a yaml mapping node with keys in sorted order. +func mapNode(m map[string]string) *yaml.Node { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + for _, k := range keys { + node.Content = append(node.Content, scalarNode(k), scalarNode(m[k])) + } + return node +} + +func scalarNode(value string) *yaml.Node { + return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value} +} + +func writeFile(gen *protogen.Plugin, name string, doc *yaml.Node) error { + f := gen.NewGeneratedFile(name, "") + enc := yaml.NewEncoder(f) + enc.SetIndent(2) + if err := enc.Encode(doc); err != nil { + return err + } + return enc.Close() +} diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.mod b/cmd/protoc-gen-nexus-rpc-yaml/go.mod new file mode 100644 index 000000000..863c771a6 --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/go.mod @@ -0,0 +1,12 @@ +module github.com/temporalio/api/cmd/protoc-gen-nexus-rpc-yaml + +go 1.25.4 + +require ( + google.golang.org/protobuf v1.36.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 + +require github.com/google/go-cmp v0.6.0 // indirect diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.sum b/cmd/protoc-gen-nexus-rpc-yaml/go.sum new file mode 100644 index 000000000..cbc5252ff --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/go.sum @@ -0,0 +1,10 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/protoc-gen-nexus-rpc-yaml/main.go b/cmd/protoc-gen-nexus-rpc-yaml/main.go new file mode 100644 index 000000000..31cdca92f --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/main.go @@ -0,0 +1,13 @@ +// protoc-gen-nexus-rpc-yaml is a protoc plugin that generates nexus/temporal-proto-models-nexusrpc.yaml +// from proto service methods annotated with option (nexusannotations.v1.operation).tags = "exposed". +package main + +import ( + "google.golang.org/protobuf/compiler/protogen" +) + +func main() { + protogen.Options{}.Run(func(gen *protogen.Plugin) error { + return generate(gen) + }) +} diff --git a/nexus/temporal-proto-models-nexusrpc.yaml b/nexus/temporal-proto-models-nexusrpc.yaml new file mode 100644 index 000000000..e0761fd15 --- /dev/null +++ b/nexus/temporal-proto-models-nexusrpc.yaml @@ -0,0 +1,19 @@ +nexusrpc: 1.0.0 +services: + WorkflowService: + operations: + SignalWithStartWorkflowExecution: + input: + $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest + $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest + $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest + $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest' + output: + $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse + $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse + $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse + $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse' From a4f8379af42ff5157bf7298fe3dad4f63668aaae Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 3 Jun 2026 15:24:11 -0700 Subject: [PATCH 09/14] Update to latest WIT --- nexus/temporal-system.wit | 67 +++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index e410c64d7..1b7f10656 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -5,6 +5,9 @@ world system { } /// @nexus.endpoint "__temporal_system" +/// @nexus.service-name "temporal.api.workflowservice.v1.WorkflowService" +/// @nexus.delay-load-temporalio-workflow +/// @nexus.experimental interface workflow-service { use nexus:temporal-types/model@1.0.0.{ duration, @@ -23,28 +26,60 @@ interface workflow-service { workflow-id-reuse-policy, }; - /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" + /// @nexus.doc "Request fields for signaling a workflow, starting it first if needed." + /// @nexus.experimental + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" typescript-package="@temporalio/proto" record signal-with-start-workflow-request { + /// @nexus.doc + /// python="Workflow type name or callable identifying the workflow to start." + /// typescript="Workflow type name or workflow function identifying the workflow to start." /// @nexus.proto-field "workflow_type" workflow: workflow-function, - workflow-id: string, + /// @nexus.doc "Unique identifier for the workflow execution." + /// @nexus.proto-field "workflow_id" + id: string, + /// @nexus.doc "Task queue to run the workflow on." task-queue: task-queue, + /// @nexus.doc + /// python="Signal name or callable to send with the start request." + /// typescript="Signal name or signal definition to send with the start request." /// @nexus.proto-field "signal_name" signal: signal-function, - workflow-execution-timeout: option, - workflow-run-timeout: option, - workflow-task-timeout: option, - identity: option, + /// @nexus.doc "Total workflow execution timeout, including retries and continue-as-new." + /// @nexus.proto-field "workflow_execution_timeout" + execution-timeout: option, + /// @nexus.doc "Timeout of a single workflow run." + /// @nexus.proto-field "workflow_run_timeout" + run-timeout: option, + /// @nexus.doc "Timeout of a single workflow task." + /// @nexus.proto-field "workflow_task_timeout" + task-timeout: option, + /// @nexus.omit + identity: placeholder, + /// @nexus.doc "Request ID used to deduplicate workflow start requests." request-id: option, - workflow-id-reuse-policy: option, - workflow-id-conflict-policy: option, + /// @nexus.doc "Behavior when a closed workflow with the same ID exists. Default is allow-duplicate." + /// @nexus.proto-field "workflow_id_reuse_policy" + /// @nexus.default "allow-duplicate" + id-reuse-policy: workflow-id-reuse-policy, + /// @nexus.doc "Behavior when a workflow is currently running with the same ID. Set to use-existing for idempotent deduplication on workflow ID. Cannot be set if id-reuse-policy is terminate-if-running." + /// @nexus.proto-field "workflow_id_conflict_policy" + id-conflict-policy: option, + /// @nexus.doc "Retry policy for the workflow." retry-policy: option, + /// @nexus.doc "Cron schedule for recurring workflow executions. See https://docs.temporal.io/cron-job." cron-schedule: option, + /// @nexus.doc "Memo for the workflow." memo: option, + /// @nexus.doc "Typed search attributes for the workflow." search-attributes: option, + /// @nexus.doc "Priority of the workflow execution." priority: option, + /// @nexus.doc "Override for workflow versioning behavior." versioning-override: option, - workflow-start-delay: option, + /// @nexus.doc "Amount of time to wait before starting the workflow. This does not work with cron-schedule." + /// @nexus.proto-field "workflow_start_delay" + start-delay: option, user-metadata: option, /// @nexus.source python="workflow_namespace" typescript="workflowNamespace" namespace: string, @@ -58,7 +93,8 @@ interface workflow-service { time-skipping-config: placeholder, } - /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" + /// @nexus.experimental + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" typescript-package="@temporalio/proto" record signal-with-start-workflow-response { run-id: option, started: option, @@ -66,12 +102,17 @@ interface workflow-service { signal-link: placeholder, } + /// @nexus.doc + /// "Signal a workflow, starting it first if needed." + /// returns="A workflow handle to the started workflow." /// @nexus.output-transform - /// python-type="workflow.ExternalWorkflowHandle[typing.Any]" - /// python="workflow.get_external_workflow_handle(request.workflow_id, run_id=result.run_id)" + /// python-type="temporalio.workflow.ExternalWorkflowHandle[WorkflowResult]" + /// python="temporalio.workflow.get_external_workflow_handle(request.id, run_id=result.run_id)" /// typescript-type="workflow.ExternalWorkflowHandle" - /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" + /// typescript="workflow.getExternalWorkflowHandle(request.id, result.runId ?? undefined)" + /// typescript-package="@temporalio/workflow" /// @nexus.operation name="SignalWithStartWorkflowExecution" + /// @nexus.experimental signal-with-start-workflow: func( request: signal-with-start-workflow-request, ) -> signal-with-start-workflow-response; From f9771a613830b1c1c910aba1f46a8447b000200c Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 3 Jun 2026 15:26:46 -0700 Subject: [PATCH 10/14] Rename file --- Makefile | 4 ++-- nexus/{temporal-system.wit => workflow-service.wit} | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) rename nexus/{temporal-system.wit => workflow-service.wit} (97%) diff --git a/Makefile b/Makefile index 8d230e741..cc7011acd 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT) OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload -SYSTEM_NEXUS_WIT := nexus/temporal-system.wit +SYSTEM_NEXUS_WIT := nexus/workflow-service.wit SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) NEXUS_API_GEN ?= nexus-api-gen @@ -124,7 +124,7 @@ buf-lint: $(STAMPDIR)/buf-mod-prune (cd $(PROTO_ROOT) && buf lint) buf-breaking: - @printf $(COLOR) "Run buf breaking changes check against master branch..." + @printf $(COLOR) "Run buf breaking changes check against master branch..." @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') nexus-rpc-yaml: nexus-rpc-yaml-install diff --git a/nexus/temporal-system.wit b/nexus/workflow-service.wit similarity index 97% rename from nexus/temporal-system.wit rename to nexus/workflow-service.wit index 1b7f10656..b318d086a 100644 --- a/nexus/temporal-system.wit +++ b/nexus/workflow-service.wit @@ -116,4 +116,7 @@ interface workflow-service { signal-with-start-workflow: func( request: signal-with-start-workflow-request, ) -> signal-with-start-workflow-response; + signal-with-start-workflow-execution: func( + request: signal-with-start-workflow-request, + ) -> signal-with-start-workflow-response; } From e7adc477347d393c5900428fbce30930f6547351 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 3 Jun 2026 15:38:50 -0700 Subject: [PATCH 11/14] Use published nex-gen tool --- Makefile | 12 +++++----- README.md | 2 +- cmd/protoc-gen-system-nexus-wit/generator.go | 23 ++++++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index f8e1304a4..ad51c1470 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ OAPI3_PATH := .components.schemas.Payload SYSTEM_NEXUS_WIT := nexus/workflow-service.wit SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) -NEXUS_API_GEN ?= nexus-api-gen +NEX_GEN ?= nex-gen $(PROTO_OUT): mkdir $(PROTO_OUT) @@ -144,11 +144,11 @@ nexus-rpc-yaml-install: @cd cmd/protoc-gen-nexus-rpc-yaml && go install . ##### Compile system Nexus WIT files ##### -system-nexus-wit: system-nexus-wit-install nexus-api-gen-install +system-nexus-wit: system-nexus-wit-install nex-gen-install printf $(COLOR) "Generate system Nexus WIT..." protoc -I $(PROTO_ROOT) \ --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ - --system-nexus-wit_opt=nexus_api_gen=$(NEXUS_API_GEN) \ + --system-nexus-wit_opt=nex_gen=$(NEX_GEN) \ --system-nexus-wit_out=. \ $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) @@ -156,9 +156,9 @@ system-nexus-wit-install: printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..." @cd cmd/protoc-gen-system-nexus-wit && go install . -nexus-api-gen-install: - printf $(COLOR) "Install nexus-api-gen if missing..." - command -v $(NEXUS_API_GEN) >/dev/null || CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/temporalio/nexus-api-gen +nex-gen-install: + printf $(COLOR) "Install nex-gen if missing..." + command -v $(NEX_GEN) >/dev/null || cargo install nex-gen ##### Clean ##### clean: diff --git a/README.md b/README.md index b31d2f70d..34a75bd19 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Install as git submodule to the project. ## Contribution Make your change to the temporal/proto files, and run `make` to update the openapi definitions. -Rust is also required because `make` installs and runs `nexus-api-gen` when regenerating system Nexus WIT files. +Rust is also required because `make` installs and runs `nex-gen` when regenerating system Nexus WIT files. ## Breaking changes diff --git a/cmd/protoc-gen-system-nexus-wit/generator.go b/cmd/protoc-gen-system-nexus-wit/generator.go index 31fdfbb43..c9ef4b031 100644 --- a/cmd/protoc-gen-system-nexus-wit/generator.go +++ b/cmd/protoc-gen-system-nexus-wit/generator.go @@ -15,9 +15,9 @@ import ( ) type params struct { - nexusAPIGen string - output string - input string + nexGen string + output string + input string } // parseParams parses the comma-separated key=value parameter string provided by protoc. @@ -25,15 +25,14 @@ type params struct { // - output: required. Path to the WIT file to generate, relative to the // --system-nexus-wit_out directory. Example: "nexus/temporal-system.wit". // -// - nexus_api_gen: optional. Path to the nexus-api-gen binary. Defaults to -// "nexus-api-gen". +// - nex_gen: optional. Path to the nex-gen binary. Defaults to "nex-gen". // // - input: optional. Existing WIT file to update. Defaults to output, so // existing handwritten annotations and type refinements are preserved when // regenerating in place. func parseParams(raw string) (params, error) { p := params{ - nexusAPIGen: "nexus-api-gen", + nexGen: "nex-gen", } if raw == "" { return p, nil @@ -44,8 +43,8 @@ func parseParams(raw string) (params, error) { return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) } switch key { - case "nexus_api_gen": - p.nexusAPIGen = value + case "nex_gen": + p.nexGen = value case "output": p.output = value case "input": @@ -97,7 +96,7 @@ func generate(gen *protogen.Plugin) error { } for _, rpc := range rpcs { - if err := runAddRPC(p.nexusAPIGen, descriptorPath, rpc, tempOutput, input); err != nil { + if err := runAddRPC(p.nexGen, descriptorPath, rpc, tempOutput, input); err != nil { return err } input = tempOutput @@ -150,7 +149,7 @@ func writeDescriptorSet(gen *protogen.Plugin, descriptorPath string) error { return os.WriteFile(descriptorPath, data, 0o644) } -func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { +func runAddRPC(nexGen string, descriptors string, rpc string, output string, input string) error { args := []string{ "add-rpc", "--descriptors", descriptors, @@ -161,11 +160,11 @@ func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string args = append(args, "--input", input) } - command := exec.Command(nexusAPIGen, args...) + command := exec.Command(nexGen, args...) command.Stdout = os.Stdout command.Stderr = os.Stderr if err := command.Run(); err != nil { - return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) + return fmt.Errorf("%s %s: %w", nexGen, strings.Join(args, " "), err) } return nil } From ef2abd3b1e91bd846a71f88fd676e3ed2abe72fd Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 3 Jun 2026 16:01:03 -0700 Subject: [PATCH 12/14] Rename tool, add temporal wit type dependency --- Makefile | 2 + cmd/protoc-gen-system-nexus-wit/generator.go | 19 ++- nexus/deps/nexus-temporal-types/model.wit | 149 +++++++++++++++++++ 3 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 nexus/deps/nexus-temporal-types/model.wit diff --git a/Makefile b/Makefile index ad51c1470..3de81b037 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,7 @@ OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload SYSTEM_NEXUS_WIT := nexus/workflow-service.wit +SYSTEM_NEXUS_WIT_DEPS := nexus/deps/nexus-temporal-types/model.wit SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) NEX_GEN ?= nex-gen @@ -149,6 +150,7 @@ system-nexus-wit: system-nexus-wit-install nex-gen-install protoc -I $(PROTO_ROOT) \ --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ --system-nexus-wit_opt=nex_gen=$(NEX_GEN) \ + --system-nexus-wit_opt=linked_input=$(SYSTEM_NEXUS_WIT_DEPS) \ --system-nexus-wit_out=. \ $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) diff --git a/cmd/protoc-gen-system-nexus-wit/generator.go b/cmd/protoc-gen-system-nexus-wit/generator.go index c9ef4b031..b30202078 100644 --- a/cmd/protoc-gen-system-nexus-wit/generator.go +++ b/cmd/protoc-gen-system-nexus-wit/generator.go @@ -15,9 +15,10 @@ import ( ) type params struct { - nexGen string - output string - input string + nexGen string + output string + input string + linkedInputs []string } // parseParams parses the comma-separated key=value parameter string provided by protoc. @@ -27,6 +28,9 @@ type params struct { // // - nex_gen: optional. Path to the nex-gen binary. Defaults to "nex-gen". // +// - linked_input: optional, repeatable. Additional WIT input passed to +// nex-gen after the main input. +// // - input: optional. Existing WIT file to update. Defaults to output, so // existing handwritten annotations and type refinements are preserved when // regenerating in place. @@ -49,6 +53,8 @@ func parseParams(raw string) (params, error) { p.output = value case "input": p.input = value + case "linked_input": + p.linkedInputs = append(p.linkedInputs, value) default: return p, fmt.Errorf("unknown parameter %q", key) } @@ -96,7 +102,7 @@ func generate(gen *protogen.Plugin) error { } for _, rpc := range rpcs { - if err := runAddRPC(p.nexGen, descriptorPath, rpc, tempOutput, input); err != nil { + if err := runAddRPC(p.nexGen, descriptorPath, rpc, tempOutput, input, p.linkedInputs); err != nil { return err } input = tempOutput @@ -149,7 +155,7 @@ func writeDescriptorSet(gen *protogen.Plugin, descriptorPath string) error { return os.WriteFile(descriptorPath, data, 0o644) } -func runAddRPC(nexGen string, descriptors string, rpc string, output string, input string) error { +func runAddRPC(nexGen string, descriptors string, rpc string, output string, input string, linkedInputs []string) error { args := []string{ "add-rpc", "--descriptors", descriptors, @@ -159,6 +165,9 @@ func runAddRPC(nexGen string, descriptors string, rpc string, output string, inp if input != "" { args = append(args, "--input", input) } + for _, linkedInput := range linkedInputs { + args = append(args, "--input", linkedInput) + } command := exec.Command(nexGen, args...) command.Stdout = os.Stdout diff --git a/nexus/deps/nexus-temporal-types/model.wit b/nexus/deps/nexus-temporal-types/model.wit new file mode 100644 index 000000000..1cc9fb8a0 --- /dev/null +++ b/nexus/deps/nexus-temporal-types/model.wit @@ -0,0 +1,149 @@ +package nexus:temporal-types@1.0.0; + +interface model { + /// String-shaped placeholder for semantic types that generators reinterpret. + type placeholder = string; + + /// @nexus.proto "temporal.api.common.v1.Payload" typescript-package="@temporalio/proto" + /// @nexus.type python="typing.Any" typescript="common.Payload" typescript-package="@temporalio/common" + type payload = placeholder; + + /// @nexus.proto "temporal.api.common.v1.Payloads" + /// typescript-package="@temporalio/proto" + type payloads = list; + + /// Callable result annotation for workflow functions. + /// @nexus.type + /// python="collections.abc.Awaitable[WorkflowResult]" + /// typescript="Promise" + type workflow-result = placeholder; + + /// Receiver/context argument for workflow callable method forms. + /// @nexus.type python="typing.Any" typescript="any" + type callable-prefix = placeholder; + + /// @nexus.function-args + /// varargs=true + /// param="args" + /// typescript-drop-prefix=true + workflow-call: async func(callable-prefix: callable-prefix, args: payloads) -> workflow-result; + + /// Callable result annotation for signal functions. + /// @nexus.type python="None | collections.abc.Awaitable[None]" typescript="void" + type signal-result = placeholder; + + /// @nexus.function-args + /// varargs=true + /// param="signal-args" + /// typescript-drop-prefix=true + signal-call: func(callable-prefix: callable-prefix, signal-args: payloads) -> signal-result; + + /// @nexus.proto "temporal.api.common.v1.WorkflowType" typescript-package="@temporalio/proto" + /// @nexus.type python="str" typescript="string" + type workflow-type = placeholder; + + /// @nexus.function + /// primary=true + /// signature="workflow-call" + /// args-field="input" + /// result-type-parameter="WorkflowResult" + /// alternate-type="workflow-type" + /// @nexus.add-rpc-compatible-with "workflow-type" + type workflow-function = placeholder; + + /// @nexus.function + /// signature="signal-call" + /// args-field="signal-input" + /// alternate-type="string" + /// python-converter="signal_function_to_proto" + /// typescript-converter="signalFunctionToProto" + /// @nexus.add-rpc-compatible-with "string" + /// @nexus.typescript-with-arguments + /// signature="signal-call" + /// args-field="signal-input" + /// alternate-type="string" + /// value-type="workflow.SignalDefinition" + /// args-type="Value extends workflow.SignalDefinition ? Args : never" + /// name-expr="value.name" + /// typescript-package="@temporalio/workflow" + type signal-function = placeholder; + + /// @nexus.proto "temporal.api.common.v1.RetryPolicy" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.RetryPolicy" + /// typescript="common.RetryPolicy" + /// typescript-package="@temporalio/common" + type retry-policy = placeholder; + + /// @nexus.proto "temporal.api.taskqueue.v1.TaskQueue" typescript-package="@temporalio/proto" + /// @nexus.type python="str" typescript="string" + type task-queue = placeholder; + + /// @nexus.proto "temporal.api.common.v1.Memo" typescript-package="@temporalio/proto" + /// @nexus.type python="collections.abc.Mapping[str, typing.Any]" typescript="Record" + type memo = placeholder; + + /// @nexus.proto "temporal.api.common.v1.SearchAttributes" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.TypedSearchAttributes" + /// typescript="common.TypedSearchAttributes" + /// typescript-package="@temporalio/common" + type search-attributes = placeholder; + + /// @nexus.proto "temporal.api.common.v1.Priority" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.Priority" + /// typescript="common.Priority" + /// typescript-package="@temporalio/common" + type priority = placeholder; + + /// @nexus.proto "temporal.api.workflow.v1.VersioningOverride" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.VersioningOverride" + /// typescript="common.VersioningOverride" + /// typescript-package="@temporalio/common" + type versioning-override = placeholder; + + /// @nexus.proto "google.protobuf.Duration" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="datetime.timedelta" + /// typescript="common.Duration" + /// typescript-package="@temporalio/common" + type duration = placeholder; + + /// @nexus.proto "temporal.api.enums.v1.WorkflowIdReusePolicy" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.WorkflowIDReusePolicy" + /// typescript="common.WorkflowIdReusePolicy" + /// typescript-package="@temporalio/common" + enum workflow-id-reuse-policy { + allow-duplicate, + allow-duplicate-failed-only, + reject-duplicate, + terminate-if-running, + } + + /// @nexus.proto "temporal.api.enums.v1.WorkflowIdConflictPolicy" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.WorkflowIDConflictPolicy" + /// typescript="common.WorkflowIdConflictPolicy" + /// typescript-package="@temporalio/common" + enum workflow-id-conflict-policy { + fail, + use-existing, + terminate-existing, + } + + /// @nexus.proto "temporal.api.sdk.v1.UserMetadata" typescript-package="@temporalio/proto" + /// @nexus.flatten-in-api + record user-metadata { + /// @nexus.doc "Single-line fixed summary for the workflow execution that may appear in UI and CLI. This can be in single-line Temporal Markdown format." + /// @nexus.proto-field "summary" + /// @nexus.flattened-type python="str" typescript="string" + static-summary: option, + /// @nexus.doc "General fixed details for the workflow execution that may appear in UI and CLI. This can be in Temporal Markdown format and can span multiple lines. This value is fixed on the workflow execution and cannot be updated." + /// @nexus.proto-field "details" + /// @nexus.flattened-type python="str" typescript="string" + static-details: option, + } +} From 6977b93fd9d7a66f76b89606798a9f990fa541d4 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 3 Jun 2026 16:19:11 -0700 Subject: [PATCH 13/14] Build workflow service WIT from workflow proto --- Makefile | 4 ++-- cmd/protoc-gen-system-nexus-wit/main.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3de81b037..7397a930a 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ OAPI3_PATH := .components.schemas.Payload SYSTEM_NEXUS_WIT := nexus/workflow-service.wit SYSTEM_NEXUS_WIT_DEPS := nexus/deps/nexus-temporal-types/model.wit -SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) +SYSTEM_NEXUS_SERVICE_PROTO_FILE := temporal/api/workflowservice/v1/service.proto NEX_GEN ?= nex-gen $(PROTO_OUT): @@ -152,7 +152,7 @@ system-nexus-wit: system-nexus-wit-install nex-gen-install --system-nexus-wit_opt=nex_gen=$(NEX_GEN) \ --system-nexus-wit_opt=linked_input=$(SYSTEM_NEXUS_WIT_DEPS) \ --system-nexus-wit_out=. \ - $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) + $(SYSTEM_NEXUS_SERVICE_PROTO_FILE) system-nexus-wit-install: printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..." diff --git a/cmd/protoc-gen-system-nexus-wit/main.go b/cmd/protoc-gen-system-nexus-wit/main.go index 0e9fe3182..fb29e5fe0 100644 --- a/cmd/protoc-gen-system-nexus-wit/main.go +++ b/cmd/protoc-gen-system-nexus-wit/main.go @@ -1,4 +1,4 @@ -// protoc-gen-system-nexus-wit generates nexus/temporal-system.wit from proto service +// protoc-gen-system-nexus-wit generates Nexus WIT from proto service // methods annotated with option (nexusannotations.v1.operation).tags = "exposed". package main From 8fa4ea8e7e1099d7569dc36b8acf050b7a416594 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 3 Jun 2026 16:23:39 -0700 Subject: [PATCH 14/14] Inline workflow WIT generation inputs --- Makefile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 7397a930a..dc8ab5025 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,6 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT) OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload -SYSTEM_NEXUS_WIT := nexus/workflow-service.wit -SYSTEM_NEXUS_WIT_DEPS := nexus/deps/nexus-temporal-types/model.wit -SYSTEM_NEXUS_SERVICE_PROTO_FILE := temporal/api/workflowservice/v1/service.proto NEX_GEN ?= nex-gen $(PROTO_OUT): @@ -148,11 +145,11 @@ nexus-rpc-yaml-install: system-nexus-wit: system-nexus-wit-install nex-gen-install printf $(COLOR) "Generate system Nexus WIT..." protoc -I $(PROTO_ROOT) \ - --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ + --system-nexus-wit_opt=output=nexus/workflow-service.wit \ --system-nexus-wit_opt=nex_gen=$(NEX_GEN) \ - --system-nexus-wit_opt=linked_input=$(SYSTEM_NEXUS_WIT_DEPS) \ + --system-nexus-wit_opt=linked_input=nexus/deps \ --system-nexus-wit_out=. \ - $(SYSTEM_NEXUS_SERVICE_PROTO_FILE) + temporal/api/workflowservice/v1/service.proto system-nexus-wit-install: printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..."