Skip to content

Commit

Permalink
.NET Support (#335)
Browse files Browse the repository at this point in the history
Fixes #226
  • Loading branch information
cretz authored Aug 30, 2023
1 parent 22c291e commit a9a5835
Show file tree
Hide file tree
Showing 18 changed files with 883 additions and 10 deletions.
10 changes: 10 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[*.cs]

##### Temporal additions ######

# Please keep in alphabetical order by field.

# Some calls we mark async, like signals, that may not doing anything async
dotnet_diagnostic.CS1998.severity = none

###############################
17 changes: 17 additions & 0 deletions .github/workflows/all-docker-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ on:
java-ver:
description: Java SDK ver to build. Skipped if not specified. Must start with v.
type: string
cs-ver:
description: .NET SDK ver to build. Skipped if not specified. Must start with v.
type: string
do-push:
description: If set, push the built images to Docker Hub.
type: boolean
Expand All @@ -40,6 +43,9 @@ on:
java-ver:
description: Java SDK ver to build. Skipped if not specified. Must start with v.
type: string
cs-ver:
description: .NET SDK ver to build. Skipped if not specified. Must start with v.
type: string
do-push:
description: If set, push the built images to Docker Hub.
type: boolean
Expand Down Expand Up @@ -94,3 +100,14 @@ jobs:
semver-latest: major
do-push: ${{ inputs.do-push }}
skip-cloud: ${{ inputs.skip-cloud }}

build-dotnet-docker-images:
if: inputs.cs-ver
uses: ./.github/workflows/docker-images.yaml
secrets: inherit
with:
lang: cs
sdk-version: ${{ inputs.cs-ver }}
semver-latest: major
do-push: ${{ inputs.do-push }}
skip-cloud: ${{ inputs.skip-cloud }}
23 changes: 23 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ jobs:

- run: ./gradlew build

build-dotnet:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest] # windows-latest - like 8x slower. Excluded for now since we're just building.
runs-on: ${{ matrix.os }}
steps:
- name: Print build information
run: 'echo head_ref: "$GITHUB_HEAD_REF", ref: "$GITHUB_REF", os: ${{ matrix.os }}'
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v3
- run: dotnet build
- run: dotnet test

feature-tests-ts:
uses: ./.github/workflows/typescript.yaml
with:
Expand Down Expand Up @@ -112,6 +126,14 @@ jobs:
features-repo-ref: ${{ github.head_ref }}
features-repo-path: ${{ github.event.pull_request.head.repo.full_name }}

feature-tests-dotnet:
uses: ./.github/workflows/dotnet.yaml
with:
version: 0.1.0-beta1
version-is-repo-ref: false
features-repo-ref: ${{ github.head_ref }}
features-repo-path: ${{ github.event.pull_request.head.repo.full_name }}

build-docker-images:
uses: ./.github/workflows/all-docker-images.yaml
secrets: inherit
Expand All @@ -122,3 +144,4 @@ jobs:
ts-ver: 'v1.5.2'
java-ver: 'v1.21.1'
py-ver: 'v1.0.0'
cs-ver: 'v0.1.0-beta1'
110 changes: 110 additions & 0 deletions .github/workflows/dotnet.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: .NET Features Testing
on:
workflow_call:
inputs:
dotnet-repo-path:
type: string
default: 'temporalio/sdk-dotnet'
version:
required: true
type: string
# When true, the version refers to a repo tag/ref. When false, NPM package version.
version-is-repo-ref:
required: true
type: boolean
features-repo-path:
type: string
default: 'temporalio/features'
features-repo-ref:
type: string
default: 'main'
# If set, download the docker image for server from the provided artifact name
docker-image-artifact-name:
type: string
required: false

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./features
steps:
- name: Print git info
run: 'echo head_ref: "$GITHUB_HEAD_REF", ref: "$GITHUB_REF", ts version: ${{ inputs.version }}'
working-directory: '.'

- name: Download docker artifacts
if: ${{ inputs.docker-image-artifact-name }}
uses: actions/download-artifact@v3
with:
name: ${{ inputs.docker-image-artifact-name }}
path: /tmp/server-docker

- name: Load server Docker image
if: ${{ inputs.docker-image-artifact-name }}
run: docker load --input /tmp/server-docker/temporal-autosetup.tar
working-directory: '.'

- name: Checkout SDK features repo
uses: actions/checkout@v3
with:
path: features
repository: ${{ inputs.features-repo-path }}
ref: ${{ inputs.features-repo-ref }}
- name: Checkout .NET SDK repo
if: ${{ inputs.version-is-repo-ref }}
uses: actions/checkout@v2
with:
repository: ${{ inputs.dotnet-repo-path }}
submodules: recursive
path: sdk-dotnet
ref: ${{ inputs.version }}

- uses: actions/setup-dotnet@v3

- name: Install protoc
if: ${{ inputs.version-is-repo-ref }}
uses: arduino/setup-protoc@v1
with:
version: '3.x'
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-go@v2
with:
go-version: '^1.19'

- uses: Swatinem/rust-cache@v1
if: ${{ inputs.version-is-repo-ref }}
with:
working-directory: sdk-dotnet/src/Temporalio/Bridge

# Build .NET SDK if using repo
# Don't build during install phase since we're going to explicitly build
- run: dotnet build
if: ${{ inputs.version-is-repo-ref }}
working-directory: ./sdk-dotnet

- name: Start containerized server and dependencies
if: inputs.docker-image-artifact-name
run: |
docker-compose \
-f /tmp/server-docker/docker-compose.yml \
-f ./dockerfiles/docker-compose.for-server-image.yaml \
up -d temporal-server cassandra elasticsearch
- name: Run SDK-features tests directly
if: inputs.docker-image-artifact-name == ''
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 }}"

# Running the tests in their own step keeps the logs readable
- name: Run containerized SDK-features tests
if: inputs.docker-image-artifact-name
run: |
docker-compose \
-f /tmp/server-docker/docker-compose.yml \
-f ./dockerfiles/docker-compose.for-server-image.yaml \
up --no-log-prefix --exit-code-from features-tests-cs features-tests-cs
- name: Tear down docker compose
if: inputs.docker-image-artifact-name && (success() || failure())
run: docker-compose -f /tmp/server-docker/docker-compose.yml -f ./dockerfiles/docker-compose.for-server-image.yaml down -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pyrightconfig.json

# Build stuff
bin
obj

# VS Code config
.vscode
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ Prerequisites:
- [Python](https://www.python.org/) 3.10+
- [Poetry](https://python-poetry.org/): `poetry install`
- `setuptools`: `python -m pip install -U setuptools`
- [.NET](https://dotnet.microsoft.com) 7+

Command:

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

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

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

`PATTERN` must match either the features relative directory _or_ the relative directory + `/feature.<ext>` via
[Go path match rules](https://pkg.go.dev/path#Match) which notably does not include recursive depth matching. If
Expand Down
2 changes: 2 additions & 0 deletions cmd/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func (p *Preparer) Prepare(ctx context.Context) error {
_, err = p.BuildTypeScriptProgram(ctx)
case "py":
_, err = p.BuildPythonProgram(ctx)
case "cs":
_, err = p.BuildDotNetProgram(ctx)
default:
err = fmt.Errorf("unrecognized language")
}
Expand Down
25 changes: 17 additions & 8 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ func (r *Runner) Run(ctx context.Context, patterns []string) error {
if err == nil {
err = r.RunPythonExternal(ctx, run)
}
case "cs":
if r.config.DirName != "" {
r.program, err = sdkbuild.DotNetProgramFromDir(filepath.Join(r.rootDir, r.config.DirName))
}
if err == nil {
err = r.RunDotNetExternal(ctx, run)
}
default:
err = fmt.Errorf("unrecognized language")
}
Expand Down Expand Up @@ -471,39 +478,41 @@ func (r *Runner) destroyTempDir() {
}

func normalizeLangName(lang string) (string, error) {
// Normalize to file extension
switch lang {
case "go", "java", "ts", "py":
// Allow the full typescript or python word, but we need to match the file
// extension for the rest of run
case "go", "java", "ts", "py", "cs":
case "typescript":
lang = "ts"
case "python":
lang = "py"
case "dotnet", "csharp":
lang = "cs"
default:
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py", lang)
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py or cs", lang)
}
return lang, nil
}

func expandLangName(lang string) (string, error) {
// Expand to lang name
switch lang {
case "go", "java", "typescript", "python":
// Allow the full typescript or python word, but we need to match the file
// extension for the rest of run
case "ts":
lang = "typescript"
case "py":
lang = "python"
case "cs":
lang = "dotnet"
default:
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py", lang)
return "", fmt.Errorf("invalid language %q, must be one of: go or java or ts or py or cs", lang)
}
return lang, nil
}

func langFlag(destination *string) *cli.StringFlag {
return &cli.StringFlag{
Name: "lang",
Usage: "SDK language to run ('go' or 'java' or 'ts' or 'py')",
Usage: "SDK language to run ('go' or 'java' or 'ts' or 'py' or 'cs')",
Required: true,
Destination: destination,
}
Expand Down
81 changes: 81 additions & 0 deletions cmd/run_dotnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cmd

import (
"context"
"fmt"
"os"
"strings"

"github.com/temporalio/features/harness/go/cmd"
"github.com/temporalio/features/sdkbuild"
)

// BuildDotNetProgram prepares a .NET run without running it. The preparer
// config directory if present is expected to be a subdirectory name just
// beneath the root directory.
func (p *Preparer) BuildDotNetProgram(ctx context.Context) (sdkbuild.Program, error) {
p.log.Info("Building .NET project", "DirName", p.config.DirName)

// Get version from dotnet.csproj if not present
version := p.config.Version
if version == "" {
csprojBytes, err := os.ReadFile("dotnet.csproj")
if err != nil {
return nil, fmt.Errorf("failed reading dotnet.csproj: %w", err)
}
const prefix = `<PackageReference Include="Temporalio" Version="`
csproj := string(csprojBytes)
beginIndex := strings.Index(csproj, prefix)
if beginIndex == -1 {
return nil, fmt.Errorf("cannot find Temporal dependency in csproj")
}
beginIndex += len(prefix)
length := strings.Index(csproj[beginIndex:], `"`)
version = csproj[beginIndex : beginIndex+length]
}

prog, err := sdkbuild.BuildDotNetProgram(ctx, sdkbuild.BuildDotNetProgramOptions{
BaseDir: p.rootDir,
DirName: p.config.DirName,
Version: version,
ProgramContents: `await Temporalio.Features.Harness.App.RunAsync(args);`,
CsprojContents: `<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\dotnet.csproj" />
</ItemGroup>
</Project>`,
})
if err != nil {
return nil, fmt.Errorf("failed preparing: %w", err)
}
return prog, nil
}

func (r *Runner) RunDotNetExternal(ctx context.Context, run *cmd.Run) error {
// If program not built, build it
if r.program == nil {
var err error
if r.program, err = NewPreparer(r.config.PrepareConfig).BuildDotNetProgram(ctx); err != nil {
return err
}
}

args := []string{"--server", r.config.Server, "--namespace", r.config.Namespace}
if r.config.ClientCertPath != "" {
args = append(args, "--client-cert-path", r.config.ClientCertPath, "--client-key-path", r.config.ClientKeyPath)
}
args = append(args, run.ToArgs()...)
cmd, err := r.program.NewCommand(ctx, args...)
if err == nil {
r.log.Debug("Running Go separately", "Args", cmd.Args)
err = cmd.Run()
}
if err != nil {
return fmt.Errorf("failed running: %w", err)
}
return nil
}
Loading

0 comments on commit a9a5835

Please sign in to comment.