Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/update-api-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Update go.temporal.io/api

# Bumps the go.temporal.io/api (temporalio/api-go) dependency to the latest main
# pseudo-version, regenerates code, and opens a PR — but only when regeneration
# actually changes tracked *.go files (a bare go.mod/go.sum churn is discarded).
#
# Intended to be triggered by temporalio/api-go on every merge to its main branch
# (mirrors how temporalio/api triggers api-go's update-proto.yml). Can also be run
# by hand from the Actions tab.

on:
workflow_dispatch:
inputs:
commit_author:
description: "Commit author username"
required: false
default: temporal-cicd
commit_author_email:
description: "Commit author email"
required: false
default: temporal-cicd@users.noreply.github.com
source_sha:
description: "api-go commit SHA that triggered this update (for the PR body)"
required: false
default: ""

permissions:
contents: read

# At most one bump in flight; a newer api-go merge supersedes an in-progress run.
concurrency:
group: update-api-go
cancel-in-progress: true

jobs:
update:
# Guard against running in forks
if: github.repository == 'temporalio/cli'
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.TEMPORAL_CICD_APP_ID }}
private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate_token.outputs.token }}
submodules: recursive

- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod

- name: Bump go.temporal.io/api and regenerate
run: |
go get go.temporal.io/api@main
go mod tidy
make gen
# Compile sanity-check; the full cross-platform matrix runs on the PR's CI.
go build ./...

- name: Detect Go source changes
id: changes
run: |
# Only a *.go change is worth a PR; a lone go.mod/go.sum pseudo-version
# bump is noise and gets reverted so the working tree is clean.
if [ -n "$(git diff --name-only -- '*.go')" ]; then
echo "go_changed=true" >> "$GITHUB_OUTPUT"
echo "Changed Go files:"
git diff --name-only -- '*.go'
else
echo "go_changed=false" >> "$GITHUB_OUTPUT"
echo "No *.go changes from the api-go bump; discarding go.mod/go.sum churn."
git checkout -- .
fi

- name: Create pull request
if: steps.changes.outputs.go_changed == 'true'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.generate_token.outputs.token }}
branch: auto/update-api-go
delete-branch: true
commit-message: "Update go.temporal.io/api to latest main"
author: ${{ github.event.inputs.commit_author }} <${{ github.event.inputs.commit_author_email }}>
committer: ${{ github.event.inputs.commit_author }} <${{ github.event.inputs.commit_author_email }}>
title: "Update go.temporal.io/api to latest main"
body: |
Automated bump of `go.temporal.io/api` to the latest `main`
pseudo-version, with regenerated code (`make gen`).

Triggered by api-go commit: ${{ github.event.inputs.source_sha || 'manual run' }}

Opened because regeneration changed tracked `*.go` files. Please review
and, if needed, fold in coordinated `go.temporal.io/sdk` / `server` bumps
before merging.
labels: dependencies
reviewers: |
spkane31
chaptersix
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

all: gen build

gen: internal/temporalcli/commands.gen.go cliext/flags.gen.go
gen: internal/temporalcli/commands.gen.go cliext/flags.gen.go internal/temporalcli/commands.system_nexus.gen.go

internal/temporalcli/commands.gen.go: internal/temporalcli/commands.yaml
go run ./cmd/gen-commands \
Expand All @@ -15,6 +15,11 @@ cliext/flags.gen.go: cliext/option-sets.yaml
-input cliext/option-sets.yaml \
-pkg cliext > $@

internal/temporalcli/commands.system_nexus.gen.go:
go run ./cmd/gen-system-nexus \
-pkg temporalcli \
-package go.temporal.io/api/workflowservice/v1/workflowservicenexus > $@

gen-docs: internal/temporalcli/commands.yaml cliext/option-sets.yaml
go run ./cmd/gen-docs \
-input internal/temporalcli/commands.yaml \
Expand Down
54 changes: 54 additions & 0 deletions cmd/gen-system-nexus/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"strings"

"github.com/temporalio/cli/internal/systemnexusgen"
)

func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}

type stringSlice []string

func (s *stringSlice) String() string { return strings.Join(*s, ",") }
func (s *stringSlice) Set(v string) error {
*s = append(*s, v)
return nil
}

func run() error {
var (
pkg string
packagePaths stringSlice
)
flag.StringVar(&pkg, "pkg", "temporalcli", "Package name for generated code")
flag.Var(&packagePaths, "package", "Nexus service import path to scan (can be specified multiple times)")
flag.Parse()

if len(packagePaths) == 0 {
return fmt.Errorf("-package flag is required")
}

ops, err := systemnexusgen.Parse(packagePaths...)
if err != nil {
return fmt.Errorf("failed parsing nexus packages: %w", err)
}

b, err := systemnexusgen.GenerateCode(pkg, ops)
if err != nil {
return fmt.Errorf("failed generating code: %w", err)
}

if _, err := os.Stdout.Write(b); err != nil {
return fmt.Errorf("failed writing output: %w", err)
}
return nil
}
130 changes: 130 additions & 0 deletions internal/systemnexusgen/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package systemnexusgen

import (
"bytes"
"fmt"
"go/format"
"text/template"
)

// GenerateCode renders the systemNexusOps map for the given operations into a
// Go source file belonging to package pkgName.
func GenerateCode(pkgName string, ops []Operation) ([]byte, error) {
im := newImportManager()
protoAlias := im.add("google.golang.org/protobuf/proto", "proto")

type entry struct {
NexusAlias string
ServiceVar string
OpField string
ReqAlias string
ReqName string
RespAlias string
RespName string
}

entries := make([]entry, 0, len(ops))
for _, op := range ops {
entries = append(entries, entry{
NexusAlias: im.add(op.NexusPkgPath, op.NexusPkgName),
ServiceVar: op.ServiceVarName,
OpField: op.OpFieldName,
ReqAlias: im.add(op.Request.PkgPath, op.Request.PkgName),
ReqName: op.Request.Name,
RespAlias: im.add(op.Response.PkgPath, op.Response.PkgName),
RespName: op.Response.Name,
})
}

data := struct {
Package string
ProtoAlias string
Imports []importEntry
Entries []entry
}{
Package: pkgName,
ProtoAlias: protoAlias,
Imports: im.entries(),
Entries: entries,
}

var buf bytes.Buffer
if err := fileTemplate.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("rendering template: %w", err)
}
formatted, err := format.Source(buf.Bytes())
if err != nil {
return nil, fmt.Errorf("formatting generated source: %w\n%s", err, buf.String())
}
return formatted, nil
}

var fileTemplate = template.Must(template.New("file").Parse(`// Code generated by gen-system-nexus. DO NOT EDIT.

package {{.Package}}

import (
{{- range .Imports}}
{{if .Aliased}}{{.Alias}} {{end}}"{{.Path}}"
{{- end}}
)

// systemNexusOps is the generated registry of known system Nexus operations on
// the __temporal_system endpoint. Regenerate with 'make gen' after bumping the
// go.temporal.io/api dependency.
var systemNexusOps = map[systemNexusOpKey]systemNexusOpTypes{
{{- range .Entries}}
{
Endpoint: temporalSystemNexusEndpoint,
Operation: {{.NexusAlias}}.{{.ServiceVar}}.{{.OpField}}.Name(),
}: {
NewRequest: func() {{$.ProtoAlias}}.Message { return &{{.ReqAlias}}.{{.ReqName}}{} },
NewResponse: func() {{$.ProtoAlias}}.Message { return &{{.RespAlias}}.{{.RespName}}{} },
},
{{- end}}
}
`))

// importEntry is a single import line in the generated file.
type importEntry struct {
Path string
Alias string
Aliased bool // true when Alias differs from the package's natural name
}

// importManager assigns collision-free aliases to imported packages, preferring
// each package's natural name.
type importManager struct {
byPath map[string]*importEntry
used map[string]bool
order []string
}

func newImportManager() *importManager {
return &importManager{byPath: map[string]*importEntry{}, used: map[string]bool{}}
}

// add registers a package path with its natural name and returns the alias to
// use when referencing it in generated code.
func (m *importManager) add(path, name string) string {
if e, ok := m.byPath[path]; ok {
return e.Alias
}
alias := name
for i := 2; m.used[alias]; i++ {
alias = fmt.Sprintf("%s%d", name, i)
}
e := &importEntry{Path: path, Alias: alias, Aliased: alias != name}
m.byPath[path] = e
m.used[alias] = true
m.order = append(m.order, path)
return alias
}

func (m *importManager) entries() []importEntry {
out := make([]importEntry, 0, len(m.order))
for _, p := range m.order {
out = append(out, *m.byPath[p])
}
return out
}
47 changes: 47 additions & 0 deletions internal/systemnexusgen/code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package systemnexusgen_test

import (
"go/parser"
"go/token"
"strings"
"testing"

"github.com/stretchr/testify/require"
"github.com/temporalio/cli/internal/systemnexusgen"
)

func TestGenerateCode(t *testing.T) {
ops := []systemnexusgen.Operation{
{
NexusPkgPath: "go.temporal.io/api/workflowservice/v1/workflowservicenexus",
NexusPkgName: "workflowservicenexus",
ServiceVarName: "TemporalAPIWorkflowserviceV1WorkflowService",
OpFieldName: "SignalWithStartWorkflowExecution",
Request: systemnexusgen.ProtoType{
PkgPath: "go.temporal.io/api/workflowservice/v1",
PkgName: "workflowservice",
Name: "SignalWithStartWorkflowExecutionRequest",
},
Response: systemnexusgen.ProtoType{
PkgPath: "go.temporal.io/api/workflowservice/v1",
PkgName: "workflowservice",
Name: "SignalWithStartWorkflowExecutionResponse",
},
},
}

out, err := systemnexusgen.GenerateCode("temporalcli", ops)
require.NoError(t, err)
src := string(out)

require.True(t, strings.HasPrefix(src, "// Code generated by gen-system-nexus. DO NOT EDIT."))
require.Contains(t, src, "package temporalcli")
require.Contains(t, src, "var systemNexusOps = map[systemNexusOpKey]systemNexusOpTypes{")
require.Contains(t, src, "Operation: workflowservicenexus.TemporalAPIWorkflowserviceV1WorkflowService.SignalWithStartWorkflowExecution.Name(),")
require.Contains(t, src, "func() proto.Message { return &workflowservice.SignalWithStartWorkflowExecutionRequest{} }")
require.Contains(t, src, "func() proto.Message { return &workflowservice.SignalWithStartWorkflowExecutionResponse{} }")

// Output must be valid, gofmt-able Go.
_, err = parser.ParseFile(token.NewFileSet(), "gen.go", out, parser.AllErrors)
require.NoError(t, err)
}
Loading
Loading