From 9874a04213385475d2dbb5972aee0433913df008 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Sun, 14 Jun 2026 23:24:07 -0700 Subject: [PATCH 1/5] support linking interfaces --- internal/cmd/tools/doclink/doclink.go | 2 +- internal/cmd/tools/doclink/doclink_test.go | 81 ++++++++++++++++++++++ internal/deployment_client.go | 4 ++ internal/error.go | 6 ++ internal/headers.go | 8 +++ internal/schedule_client.go | 8 +++ internal/session.go | 4 +- internal/workflow.go | 24 +++++++ 8 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 internal/cmd/tools/doclink/doclink_test.go diff --git a/internal/cmd/tools/doclink/doclink.go b/internal/cmd/tools/doclink/doclink.go index ea57440b3..857b6ff70 100644 --- a/internal/cmd/tools/doclink/doclink.go +++ b/internal/cmd/tools/doclink/doclink.go @@ -572,7 +572,7 @@ func isValidDefinitionWithMatch(line, private string, inGroup string, insideStru if inGroup == "const" || inGroup == "var" { return tokens[0] == private } else if inGroup == "type" { - return len(tokens) > 2 && tokens[2] == private + return len(tokens) > 1 && (tokens[0] == private || len(tokens) > 2 && tokens[2] == private) } // Handle single-line struct, variable, or function definitions diff --git a/internal/cmd/tools/doclink/doclink_test.go b/internal/cmd/tools/doclink/doclink_test.go new file mode 100644 index 000000000..5246cb65f --- /dev/null +++ b/internal/cmd/tools/doclink/doclink_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestIsValidDefinitionWithMatchGroupedInterface(t *testing.T) { + t.Parallel() + + if !isValidDefinitionWithMatch("ContextPropagator interface {", "ContextPropagator", "type", false) { + t.Fatal("expected grouped interface declaration to match exposed internal type") + } +} + +func TestIsValidDefinitionWithMatchGroupedNamedType(t *testing.T) { + t.Parallel() + + if !isValidDefinitionWithMatch("SessionState int", "SessionState", "type", false) { + t.Fatal("expected grouped named type declaration to match exposed internal type") + } +} + +func TestIsValidDefinitionWithMatchSkipsEmbeddedInterfaceMember(t *testing.T) { + t.Parallel() + + if isValidDefinitionWithMatch("SendChannel", "SendChannel", "type", false) { + t.Fatal("expected embedded interface member not to match exposed internal type") + } +} + +func TestProcessInternalAddsDocLinkForGroupedInterface(t *testing.T) { + oldChangesNeeded := changesNeeded + changesNeeded = false + t.Cleanup(func() { + changesNeeded = oldChangesNeeded + }) + + dir := t.TempDir() + path := filepath.Join(dir, "headers.go") + source := `package internal + +type ( + // ContextPropagator is an interface that determines what information from + // context to pass along. + ContextPropagator interface { + Inject() error + } +) +` + if err := os.WriteFile(path, []byte(source), 0644); err != nil { + t.Fatal(err) + } + + file, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer file.Close() + + pairs := map[string]map[string]string{ + "workflow": { + "ContextPropagator": "ContextPropagator", + }, + } + if err := processInternal(config{fix: true}, file, pairs); err != nil { + t.Fatal(err) + } + + updatedBytes, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + updated := string(updatedBytes) + want := "// Exposed as: [go.temporal.io/sdk/workflow.ContextPropagator]" + if !strings.Contains(updated, want) { + t.Fatalf("expected generated doc link %q in:\n%s", want, updated) + } +} diff --git a/internal/deployment_client.go b/internal/deployment_client.go index 9b95fc4fa..edf1b4e1f 100644 --- a/internal/deployment_client.go +++ b/internal/deployment_client.go @@ -140,6 +140,8 @@ type ( // DeploymentListIterator is an iterator for deployments. // // NOTE: Experimental + // + // Exposed as: [go.temporal.io/sdk/client.DeploymentListIterator] DeploymentListIterator interface { // HasNext - Return whether this iterator has next value. HasNext() bool @@ -269,6 +271,8 @@ type ( // DeploymentClient is the client that manages deployments. // // NOTE: Experimental + // + // Exposed as: [go.temporal.io/sdk/client.DeploymentClient] DeploymentClient interface { // Describes an existing deployment. // diff --git a/internal/error.go b/internal/error.go index 8cc39666e..828abb59b 100644 --- a/internal/error.go +++ b/internal/error.go @@ -247,6 +247,8 @@ type ( } // UnknownExternalWorkflowExecutionError can be returned when external workflow doesn't exist + // + // Exposed as: [go.temporal.io/sdk/temporal.UnknownExternalWorkflowExecutionError] UnknownExternalWorkflowExecutionError struct{} // ServerError can be returned from server. @@ -315,9 +317,13 @@ type ( // ChildWorkflowExecutionAlreadyStartedError is set as the cause of // ChildWorkflowExecutionError when failure is due the child workflow having // already started. + // + // Exposed as: [go.temporal.io/sdk/temporal.ChildWorkflowExecutionAlreadyStartedError] ChildWorkflowExecutionAlreadyStartedError struct{} // NamespaceNotFoundError is set as the cause when failure is due namespace not found. + // + // Exposed as: [go.temporal.io/sdk/temporal.NamespaceNotFoundError] NamespaceNotFoundError struct{} // WorkflowExecutionError is returned from workflow. diff --git a/internal/headers.go b/internal/headers.go index 721d02bb4..5ccfc18f1 100644 --- a/internal/headers.go +++ b/internal/headers.go @@ -10,11 +10,15 @@ import ( // HeaderWriter is an interface to write information to temporal headers type ( + // + // Exposed as: [go.temporal.io/sdk/workflow.HeaderWriter] HeaderWriter interface { Set(string, *commonpb.Payload) } // HeaderReader is an interface to read information from temporal headers + // + // Exposed as: [go.temporal.io/sdk/workflow.HeaderReader] HeaderReader interface { Get(string) (*commonpb.Payload, bool) ForEachKey(handler func(string, *commonpb.Payload) error) error @@ -22,6 +26,8 @@ type ( // ContextPropagator is an interface that determines what information from // context to pass along + // + // Exposed as: [go.temporal.io/sdk/workflow.ContextPropagator] ContextPropagator interface { // Inject injects information from a Go Context into headers Inject(context.Context, HeaderWriter) error @@ -45,6 +51,8 @@ type ( // Note that data converters may be called in non-context-aware situations to // convert payloads that may not be customized per context. Data converter // implementers should not expect or require contextual data be present. + // + // Exposed as: [go.temporal.io/sdk/workflow.ContextAware] ContextAware interface { WithWorkflowContext(ctx Context) converter.DataConverter WithContext(ctx context.Context) converter.DataConverter diff --git a/internal/schedule_client.go b/internal/schedule_client.go index bb2285e99..71c646d6c 100644 --- a/internal/schedule_client.go +++ b/internal/schedule_client.go @@ -221,6 +221,8 @@ type ( } // ScheduleAction represents an action a schedule can take. + // + // Exposed as: [go.temporal.io/sdk/client.ScheduleAction] ScheduleAction interface { isScheduleAction() } @@ -597,6 +599,8 @@ type ( } // ScheduleHandle represents a created schedule. + // + // Exposed as: [go.temporal.io/sdk/client.ScheduleHandle] ScheduleHandle interface { // GetID returns the schedule ID associated with this handle. GetID() string @@ -700,6 +704,8 @@ type ( // ScheduleListIterator represents the interface for // schedule iterator + // + // Exposed as: [go.temporal.io/sdk/client.ScheduleListIterator] ScheduleListIterator interface { // HasNext return whether this iterator has next value HasNext() bool @@ -709,6 +715,8 @@ type ( } // Client for creating Schedules and creating Schedule handles + // + // Exposed as: [go.temporal.io/sdk/client.ScheduleClient] ScheduleClient interface { // Create a new Schedule. Create(ctx context.Context, options ScheduleOptions) (ScheduleHandle, error) diff --git a/internal/session.go b/internal/session.go index 4c42e8480..4fbc4317e 100644 --- a/internal/session.go +++ b/internal/session.go @@ -51,6 +51,8 @@ type ( Taskqueue string } + // + // Exposed as: [go.temporal.io/sdk/workflow.SessionState] SessionState int sessionTokenBucket struct { @@ -71,7 +73,7 @@ type ( *sync.Mutex // doneChanMap is keyed by sessionID. CreateSession creates and stores each // channel, and CompleteSession deletes and closes it to signal session end. - doneChanMap map[string]chan struct{} + doneChanMap map[string]chan struct{} resourceID string resourceSpecificTaskqueue string sessionTokenBucket *sessionTokenBucket diff --git a/internal/workflow.go b/internal/workflow.go index df1036f66..5c006cb93 100644 --- a/internal/workflow.go +++ b/internal/workflow.go @@ -178,6 +178,8 @@ var ( type ( // SendChannel is a write only view of the Channel + // + // Exposed as: [go.temporal.io/sdk/workflow.SendChannel] SendChannel interface { // Name returns the name of the Channel. // If the Channel was retrieved from a GetSignalChannel call, Name returns the signal name. @@ -197,6 +199,8 @@ type ( } // ReceiveChannel is a read only view of the Channel + // + // Exposed as: [go.temporal.io/sdk/workflow.ReceiveChannel] ReceiveChannel interface { // Name returns the name of the Channel. // If the Channel was retrieved from a GetSignalChannel call, Name returns the signal name. @@ -252,6 +256,8 @@ type ( // Channel must be used by workflow code instead of native go channels. // Use workflow.NewChannel(ctx) method to create Channel instance. + // + // Exposed as: [go.temporal.io/sdk/workflow.Channel] Channel interface { SendChannel ReceiveChannel @@ -259,6 +265,8 @@ type ( // Selector must be used by workflow code instead of native go select. // Use workflow.NewSelector(ctx) to create a selector. + // + // Exposed as: [go.temporal.io/sdk/workflow.Selector] Selector interface { // AddReceive registers a callback function to be called when a channel has a message to receive. // The callback is called when Select(ctx) is called. @@ -290,6 +298,8 @@ type ( // WaitGroup must be used instead of native go sync.WaitGroup by // workflow code. Use workflow.NewWaitGroup(ctx) method to create // a new WaitGroup instance + // + // Exposed as: [go.temporal.io/sdk/workflow.WaitGroup] WaitGroup interface { // Add adds delta, which may be negative, to the WaitGroup task counter. // If the counter becomes zero, all goroutines blocked on WaitGroup.Wait are released. @@ -313,6 +323,8 @@ type ( // Mutex must be used instead of native go sync.Mutex by // workflow code. Use workflow.NewMutex(ctx) method to create // a new Mutex instance + // + // Exposed as: [go.temporal.io/sdk/workflow.Mutex] Mutex interface { // Lock blocks until the mutex is acquired. // Returns CanceledError if the ctx is canceled. @@ -330,6 +342,8 @@ type ( // Semaphore must be used instead of semaphore.Weighted by // workflow code. Use workflow.NewSemaphore(ctx) method to create // a new Semaphore instance + // + // Exposed as: [go.temporal.io/sdk/workflow.Semaphore] Semaphore interface { // Acquire acquires the semaphore with a weight of n. // On success, returns nil. On failure, returns CanceledError and leaves the semaphore unchanged. @@ -342,6 +356,8 @@ type ( } // Future represents the result of an asynchronous computation. + // + // Exposed as: [go.temporal.io/sdk/workflow.Future] Future interface { // Get blocks until the future is ready. When ready it either returns non nil error or assigns result value to // the provided pointer. @@ -366,6 +382,8 @@ type ( // Settable is used to set value or error on a future. // See more: workflow.NewFuture(ctx). + // + // Exposed as: [go.temporal.io/sdk/workflow.Settable] Settable interface { Set(value interface{}, err error) SetValue(value interface{}) @@ -374,6 +392,8 @@ type ( } // ChildWorkflowFuture represents the result of a child workflow execution + // + // Exposed as: [go.temporal.io/sdk/workflow.ChildWorkflowFuture] ChildWorkflowFuture interface { Future // GetChildWorkflowExecution returns a future that will be ready when child workflow execution started. You can @@ -411,6 +431,8 @@ type ( dataConverter converter.DataConverter } // Version represents a change version. See GetVersion call. + // + // Exposed as: [go.temporal.io/sdk/workflow.Version] Version int // ChildWorkflowOptions stores all child workflow specific parameters that will be stored inside of a Context. @@ -582,6 +604,8 @@ type ( } // DynamicRegisterActivityOptions consists of options for registering a dynamic activity + // + // Exposed as: [go.temporal.io/sdk/activity.DynamicRegisterOptions] DynamicRegisterActivityOptions struct{} // DynamicRuntimeWorkflowOptions are options for a dynamic workflow. From 46c8d6b9908070dcb3616a41eb2fc2bd823ac973 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 15 Jun 2026 10:12:29 -0700 Subject: [PATCH 2/5] go run . check --- internal/cmd/tools/doclink/doclink_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/cmd/tools/doclink/doclink_test.go b/internal/cmd/tools/doclink/doclink_test.go index 5246cb65f..fb58b8178 100644 --- a/internal/cmd/tools/doclink/doclink_test.go +++ b/internal/cmd/tools/doclink/doclink_test.go @@ -58,7 +58,6 @@ type ( if err != nil { t.Fatal(err) } - defer file.Close() pairs := map[string]map[string]string{ "workflow": { @@ -68,6 +67,9 @@ type ( if err := processInternal(config{fix: true}, file, pairs); err != nil { t.Fatal(err) } + if err := file.Close(); err != nil { + t.Fatal(err) + } updatedBytes, err := os.ReadFile(path) if err != nil { From 2c72c67a7e2d8a9c59b248549c7d39f915a5127f Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 18 Jun 2026 12:29:28 -0700 Subject: [PATCH 3/5] Fix bug with comments after comments after an embedded interface member inside a grouped type/interface block --- internal/cmd/tools/doclink/doclink.go | 2 + internal/cmd/tools/doclink/doclink_test.go | 58 ++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/internal/cmd/tools/doclink/doclink.go b/internal/cmd/tools/doclink/doclink.go index 86f842972..342b10e2e 100644 --- a/internal/cmd/tools/doclink/doclink.go +++ b/internal/cmd/tools/doclink/doclink.go @@ -556,6 +556,8 @@ func isValidDefinitionWithMatch(line, private string, inGroup string, insideStru return false } + line, _, _ = strings.Cut(line, "//") + line = strings.TrimSpace(line) tokens := strings.Fields(line) if strings.HasPrefix(line, "func "+private+"(") { return true diff --git a/internal/cmd/tools/doclink/doclink_test.go b/internal/cmd/tools/doclink/doclink_test.go index fb58b8178..edfeeddbf 100644 --- a/internal/cmd/tools/doclink/doclink_test.go +++ b/internal/cmd/tools/doclink/doclink_test.go @@ -31,6 +31,14 @@ func TestIsValidDefinitionWithMatchSkipsEmbeddedInterfaceMember(t *testing.T) { } } +func TestIsValidDefinitionWithMatchSkipsCommentedEmbeddedInterfaceMember(t *testing.T) { + t.Parallel() + + if isValidDefinitionWithMatch("SendChannel // embedded channel", "SendChannel", "type", false) { + t.Fatal("expected commented embedded interface member not to match exposed internal type") + } +} + func TestProcessInternalAddsDocLinkForGroupedInterface(t *testing.T) { oldChangesNeeded := changesNeeded changesNeeded = false @@ -81,3 +89,53 @@ type ( t.Fatalf("expected generated doc link %q in:\n%s", want, updated) } } + +func TestProcessInternalDoesNotAddDocLinkForCommentedEmbeddedInterfaceMember(t *testing.T) { + oldChangesNeeded := changesNeeded + changesNeeded = false + t.Cleanup(func() { + changesNeeded = oldChangesNeeded + }) + + dir := t.TempDir() + path := filepath.Join(dir, "workflow.go") + source := `package internal + +type ( + // Channel allows sending and receiving values. + Channel interface { + SendChannel // embedded write-only view + } +) +` + if err := os.WriteFile(path, []byte(source), 0644); err != nil { + t.Fatal(err) + } + + file, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + + pairs := map[string]map[string]string{ + "workflow": { + "SendChannel": "SendChannel", + }, + } + if err := processInternal(config{fix: true}, file, pairs); err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + + updatedBytes, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + updated := string(updatedBytes) + unwanted := "// Exposed as: [go.temporal.io/sdk/workflow.SendChannel]" + if strings.Contains(updated, unwanted) { + t.Fatalf("did not expect generated doc link %q in:\n%s", unwanted, updated) + } +} From dc0ac38411eec56423a0d43dba6d9f826a48266d Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 18 Jun 2026 13:19:00 -0700 Subject: [PATCH 4/5] fix windows issue --- internal/cmd/tools/doclink/doclink.go | 16 ++++++++++------ internal/cmd/tools/doclink/doclink_test.go | 6 ------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/cmd/tools/doclink/doclink.go b/internal/cmd/tools/doclink/doclink.go index 342b10e2e..caf3f4e54 100644 --- a/internal/cmd/tools/doclink/doclink.go +++ b/internal/cmd/tools/doclink/doclink.go @@ -116,19 +116,20 @@ func run() error { if err != nil { return fmt.Errorf("failed to read file %s: %v", path, err) } - defer func() { - err = file.Close() - if err != nil { - log.Fatalf("failed to close file %s: %v", path, err) - } - }() err = processInternal(cfg, file, publicToInternal) if err != nil { return fmt.Errorf("error while parsing internal files: %v", err) } + file, err = os.Open(path) + if err != nil { + return fmt.Errorf("failed to read file %s: %v", path, err) + } err = checkInternalDocs(path, file, publicToInternal) + if closeErr := file.Close(); closeErr != nil { + return fmt.Errorf("failed to close file %s: %v", path, closeErr) + } if err != nil { return fmt.Errorf("error while checking internal docs: %v", err) } @@ -477,6 +478,9 @@ func processInternal(cfg config, file *os.File, pairs map[string]map[string]stri } newFile += nextLine + "\n" + if err := file.Close(); err != nil { + return fmt.Errorf("failed to close file %s: %v", file.Name(), err) + } if changesMade { absPath, err := filepath.Abs(file.Name()) diff --git a/internal/cmd/tools/doclink/doclink_test.go b/internal/cmd/tools/doclink/doclink_test.go index edfeeddbf..0c4835358 100644 --- a/internal/cmd/tools/doclink/doclink_test.go +++ b/internal/cmd/tools/doclink/doclink_test.go @@ -75,9 +75,6 @@ type ( if err := processInternal(config{fix: true}, file, pairs); err != nil { t.Fatal(err) } - if err := file.Close(); err != nil { - t.Fatal(err) - } updatedBytes, err := os.ReadFile(path) if err != nil { @@ -125,9 +122,6 @@ type ( if err := processInternal(config{fix: true}, file, pairs); err != nil { t.Fatal(err) } - if err := file.Close(); err != nil { - t.Fatal(err) - } updatedBytes, err := os.ReadFile(path) if err != nil { From 582b49ab7b950c315252363b017f8c09d67a760b Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 25 Jun 2026 09:25:00 -0400 Subject: [PATCH 5/5] make pollStarted buffered --- internal/internal_worker_base_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/internal_worker_base_test.go b/internal/internal_worker_base_test.go index 6370d1826..e5490f922 100644 --- a/internal/internal_worker_base_test.go +++ b/internal/internal_worker_base_test.go @@ -549,7 +549,7 @@ func (noopTaskProcessor) ProcessTask(any) error { return nil } // processed rather than silently dropped. func TestTaskNotDroppedDuringShutdown(t *testing.T) { taskProcessed := make(chan struct{}, 1) - pollStarted := make(chan struct{}) + pollStarted := make(chan struct{}, 1) // A poller that blocks until returnTask is closed, then returns a task // exactly once. Subsequent polls return nil so the poller can exit. @@ -619,7 +619,7 @@ func TestTaskNotDroppedDuringShutdown(t *testing.T) { func TestAutoscalingTaskNotDroppedDuringShutdown(t *testing.T) { taskProcessed := make(chan struct{}, 1) - pollStarted := make(chan struct{}) + pollStarted := make(chan struct{}, 1) tp := &shutdownTaskPoller{ pollStarted: pollStarted, returnTask: make(chan struct{}), @@ -816,7 +816,7 @@ func TestTaskNotProcessedDuringLegacyShutdown(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { taskProcessed := make(chan struct{}, 1) - pollStarted := make(chan struct{}) + pollStarted := make(chan struct{}, 1) // This poller simulates a poll returning a task after shutdown has // already started. Legacy shutdown should not dispatch that task. @@ -904,7 +904,7 @@ func (p *recordingTaskProcessor) ProcessTask(any) error { } func TestStopTimeoutBoundsPollerDrain(t *testing.T) { - pollStarted := make(chan struct{}) + pollStarted := make(chan struct{}, 1) releasePoll := make(chan struct{}) var releasePollOnce sync.Once releaseBlockedPoller := func() { @@ -964,7 +964,7 @@ func TestStopTimeoutBoundsPollerDrain(t *testing.T) { } func TestLegacyStopReturnsPromptlyWithBlockedPoller(t *testing.T) { - pollStarted := make(chan struct{}) + pollStarted := make(chan struct{}, 1) tp := &stopAwareShutdownPoller{ pollStarted: pollStarted, }