Skip to content

Commit a9a5835

Browse files
authored
.NET Support (#335)
Fixes #226
1 parent 22c291e commit a9a5835

File tree

18 files changed

+883
-10
lines changed

18 files changed

+883
-10
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[*.cs]
2+
3+
##### Temporal additions ######
4+
5+
# Please keep in alphabetical order by field.
6+
7+
# Some calls we mark async, like signals, that may not doing anything async
8+
dotnet_diagnostic.CS1998.severity = none
9+
10+
###############################

.github/workflows/all-docker-images.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ on:
1616
java-ver:
1717
description: Java SDK ver to build. Skipped if not specified. Must start with v.
1818
type: string
19+
cs-ver:
20+
description: .NET SDK ver to build. Skipped if not specified. Must start with v.
21+
type: string
1922
do-push:
2023
description: If set, push the built images to Docker Hub.
2124
type: boolean
@@ -40,6 +43,9 @@ on:
4043
java-ver:
4144
description: Java SDK ver to build. Skipped if not specified. Must start with v.
4245
type: string
46+
cs-ver:
47+
description: .NET SDK ver to build. Skipped if not specified. Must start with v.
48+
type: string
4349
do-push:
4450
description: If set, push the built images to Docker Hub.
4551
type: boolean
@@ -94,3 +100,14 @@ jobs:
94100
semver-latest: major
95101
do-push: ${{ inputs.do-push }}
96102
skip-cloud: ${{ inputs.skip-cloud }}
103+
104+
build-dotnet-docker-images:
105+
if: inputs.cs-ver
106+
uses: ./.github/workflows/docker-images.yaml
107+
secrets: inherit
108+
with:
109+
lang: cs
110+
sdk-version: ${{ inputs.cs-ver }}
111+
semver-latest: major
112+
do-push: ${{ inputs.do-push }}
113+
skip-cloud: ${{ inputs.skip-cloud }}

.github/workflows/ci.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ jobs:
8080

8181
- run: ./gradlew build
8282

83+
build-dotnet:
84+
strategy:
85+
fail-fast: true
86+
matrix:
87+
os: [ubuntu-latest] # windows-latest - like 8x slower. Excluded for now since we're just building.
88+
runs-on: ${{ matrix.os }}
89+
steps:
90+
- name: Print build information
91+
run: 'echo head_ref: "$GITHUB_HEAD_REF", ref: "$GITHUB_REF", os: ${{ matrix.os }}'
92+
- uses: actions/checkout@v2
93+
- uses: actions/setup-dotnet@v3
94+
- run: dotnet build
95+
- run: dotnet test
96+
8397
feature-tests-ts:
8498
uses: ./.github/workflows/typescript.yaml
8599
with:
@@ -112,6 +126,14 @@ jobs:
112126
features-repo-ref: ${{ github.head_ref }}
113127
features-repo-path: ${{ github.event.pull_request.head.repo.full_name }}
114128

129+
feature-tests-dotnet:
130+
uses: ./.github/workflows/dotnet.yaml
131+
with:
132+
version: 0.1.0-beta1
133+
version-is-repo-ref: false
134+
features-repo-ref: ${{ github.head_ref }}
135+
features-repo-path: ${{ github.event.pull_request.head.repo.full_name }}
136+
115137
build-docker-images:
116138
uses: ./.github/workflows/all-docker-images.yaml
117139
secrets: inherit
@@ -122,3 +144,4 @@ jobs:
122144
ts-ver: 'v1.5.2'
123145
java-ver: 'v1.21.1'
124146
py-ver: 'v1.0.0'
147+
cs-ver: 'v0.1.0-beta1'

.github/workflows/dotnet.yaml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: .NET Features Testing
2+
on:
3+
workflow_call:
4+
inputs:
5+
dotnet-repo-path:
6+
type: string
7+
default: 'temporalio/sdk-dotnet'
8+
version:
9+
required: true
10+
type: string
11+
# When true, the version refers to a repo tag/ref. When false, NPM package version.
12+
version-is-repo-ref:
13+
required: true
14+
type: boolean
15+
features-repo-path:
16+
type: string
17+
default: 'temporalio/features'
18+
features-repo-ref:
19+
type: string
20+
default: 'main'
21+
# If set, download the docker image for server from the provided artifact name
22+
docker-image-artifact-name:
23+
type: string
24+
required: false
25+
26+
jobs:
27+
test:
28+
runs-on: ubuntu-latest
29+
defaults:
30+
run:
31+
working-directory: ./features
32+
steps:
33+
- name: Print git info
34+
run: 'echo head_ref: "$GITHUB_HEAD_REF", ref: "$GITHUB_REF", ts version: ${{ inputs.version }}'
35+
working-directory: '.'
36+
37+
- name: Download docker artifacts
38+
if: ${{ inputs.docker-image-artifact-name }}
39+
uses: actions/download-artifact@v3
40+
with:
41+
name: ${{ inputs.docker-image-artifact-name }}
42+
path: /tmp/server-docker
43+
44+
- name: Load server Docker image
45+
if: ${{ inputs.docker-image-artifact-name }}
46+
run: docker load --input /tmp/server-docker/temporal-autosetup.tar
47+
working-directory: '.'
48+
49+
- name: Checkout SDK features repo
50+
uses: actions/checkout@v3
51+
with:
52+
path: features
53+
repository: ${{ inputs.features-repo-path }}
54+
ref: ${{ inputs.features-repo-ref }}
55+
- name: Checkout .NET SDK repo
56+
if: ${{ inputs.version-is-repo-ref }}
57+
uses: actions/checkout@v2
58+
with:
59+
repository: ${{ inputs.dotnet-repo-path }}
60+
submodules: recursive
61+
path: sdk-dotnet
62+
ref: ${{ inputs.version }}
63+
64+
- uses: actions/setup-dotnet@v3
65+
66+
- name: Install protoc
67+
if: ${{ inputs.version-is-repo-ref }}
68+
uses: arduino/setup-protoc@v1
69+
with:
70+
version: '3.x'
71+
repo-token: ${{ secrets.GITHUB_TOKEN }}
72+
- uses: actions/setup-go@v2
73+
with:
74+
go-version: '^1.19'
75+
76+
- uses: Swatinem/rust-cache@v1
77+
if: ${{ inputs.version-is-repo-ref }}
78+
with:
79+
working-directory: sdk-dotnet/src/Temporalio/Bridge
80+
81+
# Build .NET SDK if using repo
82+
# Don't build during install phase since we're going to explicitly build
83+
- run: dotnet build
84+
if: ${{ inputs.version-is-repo-ref }}
85+
working-directory: ./sdk-dotnet
86+
87+
- name: Start containerized server and dependencies
88+
if: inputs.docker-image-artifact-name
89+
run: |
90+
docker-compose \
91+
-f /tmp/server-docker/docker-compose.yml \
92+
-f ./dockerfiles/docker-compose.for-server-image.yaml \
93+
up -d temporal-server cassandra elasticsearch
94+
95+
- name: Run SDK-features tests directly
96+
if: inputs.docker-image-artifact-name == ''
97+
run: go run . run --lang cs ${{ inputs.docker-image-artifact-name && '--server localhost:7233 --namespace default' || ''}} --version "${{ inputs.version-is-repo-ref && '$(realpath ../sdk-dotnet)' || inputs.version }}"
98+
99+
# Running the tests in their own step keeps the logs readable
100+
- name: Run containerized SDK-features tests
101+
if: inputs.docker-image-artifact-name
102+
run: |
103+
docker-compose \
104+
-f /tmp/server-docker/docker-compose.yml \
105+
-f ./dockerfiles/docker-compose.for-server-image.yaml \
106+
up --no-log-prefix --exit-code-from features-tests-cs features-tests-cs
107+
108+
- name: Tear down docker compose
109+
if: inputs.docker-image-artifact-name && (success() || failure())
110+
run: docker-compose -f /tmp/server-docker/docker-compose.yml -f ./dockerfiles/docker-compose.for-server-image.yaml down -v

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pyrightconfig.json
2020

2121
# Build stuff
2222
bin
23+
obj
2324

2425
# VS Code config
2526
.vscode

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ Prerequisites:
3030
- [Python](https://www.python.org/) 3.10+
3131
- [Poetry](https://python-poetry.org/): `poetry install`
3232
- `setuptools`: `python -m pip install -U setuptools`
33+
- [.NET](https://dotnet.microsoft.com) 7+
3334

3435
Command:
3536

3637
temporal-features run --lang LANG [--version VERSION] [PATTERN...]
3738

3839
Note, `go run .` can be used in place of `go build` + `temporal-features` to save on the build step.
3940

40-
`LANG` can be `go`, `java`, `ts`, or `py`. `VERSION` is per SDK and if left off, uses the latest version set for the
41-
language in this repository.
41+
`LANG` can be `go`, `java`, `ts`, `py`, or `cs`. `VERSION` is per SDK and if left off, uses the latest version set for
42+
the language in this repository.
4243

4344
`PATTERN` must match either the features relative directory _or_ the relative directory + `/feature.<ext>` via
4445
[Go path match rules](https://pkg.go.dev/path#Match) which notably does not include recursive depth matching. If

cmd/prepare.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ func (p *Preparer) Prepare(ctx context.Context) error {
9292
_, err = p.BuildTypeScriptProgram(ctx)
9393
case "py":
9494
_, err = p.BuildPythonProgram(ctx)
95+
case "cs":
96+
_, err = p.BuildDotNetProgram(ctx)
9597
default:
9698
err = fmt.Errorf("unrecognized language")
9799
}

cmd/run.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,13 @@ func (r *Runner) Run(ctx context.Context, patterns []string) error {
278278
if err == nil {
279279
err = r.RunPythonExternal(ctx, run)
280280
}
281+
case "cs":
282+
if r.config.DirName != "" {
283+
r.program, err = sdkbuild.DotNetProgramFromDir(filepath.Join(r.rootDir, r.config.DirName))
284+
}
285+
if err == nil {
286+
err = r.RunDotNetExternal(ctx, run)
287+
}
281288
default:
282289
err = fmt.Errorf("unrecognized language")
283290
}
@@ -471,39 +478,41 @@ func (r *Runner) destroyTempDir() {
471478
}
472479

473480
func normalizeLangName(lang string) (string, error) {
481+
// Normalize to file extension
474482
switch lang {
475-
case "go", "java", "ts", "py":
476-
// Allow the full typescript or python word, but we need to match the file
477-
// extension for the rest of run
483+
case "go", "java", "ts", "py", "cs":
478484
case "typescript":
479485
lang = "ts"
480486
case "python":
481487
lang = "py"
488+
case "dotnet", "csharp":
489+
lang = "cs"
482490
default:
483-
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py", lang)
491+
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py or cs", lang)
484492
}
485493
return lang, nil
486494
}
487495

488496
func expandLangName(lang string) (string, error) {
497+
// Expand to lang name
489498
switch lang {
490499
case "go", "java", "typescript", "python":
491-
// Allow the full typescript or python word, but we need to match the file
492-
// extension for the rest of run
493500
case "ts":
494501
lang = "typescript"
495502
case "py":
496503
lang = "python"
504+
case "cs":
505+
lang = "dotnet"
497506
default:
498-
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py", lang)
507+
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py or cs", lang)
499508
}
500509
return lang, nil
501510
}
502511

503512
func langFlag(destination *string) *cli.StringFlag {
504513
return &cli.StringFlag{
505514
Name: "lang",
506-
Usage: "SDK language to run ('go' or 'java' or 'ts' or 'py')",
515+
Usage: "SDK language to run ('go' or 'java' or 'ts' or 'py' or 'cs')",
507516
Required: true,
508517
Destination: destination,
509518
}

cmd/run_dotnet.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/temporalio/features/harness/go/cmd"
10+
"github.com/temporalio/features/sdkbuild"
11+
)
12+
13+
// BuildDotNetProgram prepares a .NET run without running it. The preparer
14+
// config directory if present is expected to be a subdirectory name just
15+
// beneath the root directory.
16+
func (p *Preparer) BuildDotNetProgram(ctx context.Context) (sdkbuild.Program, error) {
17+
p.log.Info("Building .NET project", "DirName", p.config.DirName)
18+
19+
// Get version from dotnet.csproj if not present
20+
version := p.config.Version
21+
if version == "" {
22+
csprojBytes, err := os.ReadFile("dotnet.csproj")
23+
if err != nil {
24+
return nil, fmt.Errorf("failed reading dotnet.csproj: %w", err)
25+
}
26+
const prefix = `<PackageReference Include="Temporalio" Version="`
27+
csproj := string(csprojBytes)
28+
beginIndex := strings.Index(csproj, prefix)
29+
if beginIndex == -1 {
30+
return nil, fmt.Errorf("cannot find Temporal dependency in csproj")
31+
}
32+
beginIndex += len(prefix)
33+
length := strings.Index(csproj[beginIndex:], `"`)
34+
version = csproj[beginIndex : beginIndex+length]
35+
}
36+
37+
prog, err := sdkbuild.BuildDotNetProgram(ctx, sdkbuild.BuildDotNetProgramOptions{
38+
BaseDir: p.rootDir,
39+
DirName: p.config.DirName,
40+
Version: version,
41+
ProgramContents: `await Temporalio.Features.Harness.App.RunAsync(args);`,
42+
CsprojContents: `<Project Sdk="Microsoft.NET.Sdk">
43+
<PropertyGroup>
44+
<OutputType>Exe</OutputType>
45+
<TargetFramework>net7.0</TargetFramework>
46+
</PropertyGroup>
47+
<ItemGroup>
48+
<ProjectReference Include="..\dotnet.csproj" />
49+
</ItemGroup>
50+
</Project>`,
51+
})
52+
if err != nil {
53+
return nil, fmt.Errorf("failed preparing: %w", err)
54+
}
55+
return prog, nil
56+
}
57+
58+
func (r *Runner) RunDotNetExternal(ctx context.Context, run *cmd.Run) error {
59+
// If program not built, build it
60+
if r.program == nil {
61+
var err error
62+
if r.program, err = NewPreparer(r.config.PrepareConfig).BuildDotNetProgram(ctx); err != nil {
63+
return err
64+
}
65+
}
66+
67+
args := []string{"--server", r.config.Server, "--namespace", r.config.Namespace}
68+
if r.config.ClientCertPath != "" {
69+
args = append(args, "--client-cert-path", r.config.ClientCertPath, "--client-key-path", r.config.ClientKeyPath)
70+
}
71+
args = append(args, run.ToArgs()...)
72+
cmd, err := r.program.NewCommand(ctx, args...)
73+
if err == nil {
74+
r.log.Debug("Running Go separately", "Args", cmd.Args)
75+
err = cmd.Run()
76+
}
77+
if err != nil {
78+
return fmt.Errorf("failed running: %w", err)
79+
}
80+
return nil
81+
}

0 commit comments

Comments
 (0)