Skip to content
Merged
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
8 changes: 5 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 2

# templates/go uses go.mod.embed, which Dependabot cannot target. Its updates
# come from the Go example through the sync-go-template-deps command.
# The Go templates use go.mod.embed, which Dependabot cannot target. Their
# updates come from the Go examples through the sync-go-template-deps command.
multi-ecosystem-groups:
hatchet-sdk-examples:
schedule:
Expand All @@ -23,7 +23,9 @@ updates:
patterns: ["*"]

- package-ecosystem: "gomod"
directory: "/examples/simple-go"
directories:
- "/examples/simple-go"
- "/examples/use-cases/scheduled/go"
schedule:
interval: "weekly"
day: "monday"
Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
# hatchet-quickstarts

Quickstart templates and generated examples for Hatchet.
This repository owns the `hatchet quickstart` CLI templates and the generated
examples.

This repository will own the `hatchet quickstart` CLI template source, generated
cloneable examples, dependency automation, and template validation.
## Template layout

Templates live under `templates/`, are embedded into the module, and are
returned by `TemplatesFS()`. The Hatchet CLI templater reads fixed paths from
this tree, so the layout is a contract rather than an internal detail.

The default simple quickstart lives at the language root:

- `templates/go/` for Go.
- `templates/<language>/shared/` plus `templates/<language>/<package-manager>/`
for Python and TypeScript, where the package-manager directory is overlaid on
shared.

The main repo templater hardcodes these `templates/<language>/...` paths, so the
default templates must not move while released CLI versions depend on them.
Use-case templates therefore go under a separate subtree that mirrors the same
structure one level down:

- `templates/use-cases/<use-case>/go/`
- `templates/use-cases/<use-case>/<language>/shared/` plus
`templates/use-cases/<use-case>/<language>/<package-manager>/`

This keeps the default quickstart untouched, avoids collisions with language
directory names, and lets a future `--use-case` path builder reuse the existing
overlay of shared and package-manager directories with a different root.

The first use case is `scheduled`, a Go template at
`templates/use-cases/scheduled/go/`, with a generated example at
`examples/use-cases/scheduled/go/`. It registers a cron schedule with the Go
SDK `WithWorkflowCron` option. The `--use-case` flag itself lives in the main
Hatchet CLI and is not part of this repo yet.
51 changes: 33 additions & 18 deletions cmd/generate-examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@ type data struct {
}

type variant struct {
name string // output directory name, e.g. "simple-go"
name string // template data Name, used by {{ .Name }} in templates
language string
packageManager string
useCase string // empty for the default simple templates
outputDir string // path under examples/, e.g. "simple-go" or "use-cases/scheduled/go"
}

// poetry and pnpm are the generated variants because their templates carry lockfiles.
// poetry and pnpm are the generated package-manager variants because their
// templates carry lockfiles. scheduled-go is the first use-case variant.
var variants = []variant{
{name: "simple-go", language: "go", packageManager: "go"},
{name: "simple-python", language: "python", packageManager: "poetry"},
{name: "simple-typescript", language: "typescript", packageManager: "pnpm"},
{name: "simple-go", language: "go", packageManager: "go", outputDir: "simple-go"},
{name: "simple-python", language: "python", packageManager: "poetry", outputDir: "simple-python"},
{name: "simple-typescript", language: "typescript", packageManager: "pnpm", outputDir: "simple-typescript"},
{name: "scheduled-go", language: "go", packageManager: "go", useCase: "scheduled", outputDir: filepath.Join("use-cases", "scheduled", "go")},
}

const examplesDir = "examples"
Expand All @@ -68,9 +72,9 @@ func run(check bool) error {
// runGenerate rewrites examples/ in place, clearing stale output first.
func runGenerate(fsys fs.FS) error {
for _, v := range variants {
dst := filepath.Join(examplesDir, v.name)
dst := filepath.Join(examplesDir, v.outputDir)
if err := generateVariant(fsys, v, dst); err != nil {
return fmt.Errorf("generating %s: %w", v.name, err)
return fmt.Errorf("generating %s: %w", v.outputDir, err)
}
}
return nil
Expand All @@ -87,11 +91,11 @@ func runCheck(fsys fs.FS) error {

var diffs []string
for _, v := range variants {
want := filepath.Join(tmpRoot, v.name)
want := filepath.Join(tmpRoot, v.outputDir)
if err := generateVariant(fsys, v, want); err != nil {
return fmt.Errorf("generating %s: %w", v.name, err)
return fmt.Errorf("generating %s: %w", v.outputDir, err)
}
got := filepath.Join(examplesDir, v.name)
got := filepath.Join(examplesDir, v.outputDir)
diffs = append(diffs, compareTrees(want, got)...)
}

Expand All @@ -108,19 +112,30 @@ func generateVariant(fsys fs.FS, v variant, dst string) error {
return err
}
d := data{Name: v.name, PackageManager: v.packageManager}
return processMultiSource(fsys, v.language, v.packageManager, dst, d)
return processMultiSource(fsys, v.useCase, v.language, v.packageManager, dst, d)
}

// processMultiSource mirrors templater.ProcessMultiSource. Go reads its language
// directory directly. Python and TypeScript overlay the package-manager
// directory on shared.
func processMultiSource(fsys fs.FS, language, packageManager, dstDir string, d data) error {
// templateRoot is the embedded path under which a use case's templates live.
// The default simple templates live under templates; use-case templates live
// under templates/use-cases/<use-case>.
func templateRoot(useCase string) string {
if useCase == "" {
return "templates"
}
return path.Join("templates", "use-cases", useCase)
}

// processMultiSource mirrors templater.ProcessMultiSource, reading from the
// root for the given use case. Go reads its language directory directly. Python
// and TypeScript overlay the package-manager directory on shared.
func processMultiSource(fsys fs.FS, useCase, language, packageManager, dstDir string, d data) error {
root := templateRoot(useCase)
if language == "go" {
return process(fsys, "templates/go", dstDir, d)
return process(fsys, path.Join(root, "go"), dstDir, d)
}

sharedDir := path.Join("templates", language, "shared")
pkgMgrDir := path.Join("templates", language, packageManager)
sharedDir := path.Join(root, language, "shared")
pkgMgrDir := path.Join(root, language, packageManager)

if err := process(fsys, sharedDir, dstDir, d); err != nil {
return err
Expand Down
12 changes: 7 additions & 5 deletions cmd/sync-go-template-deps/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Command sync-go-template-deps keeps the embedded Go template dependency files
// in sync with the generated Go example.
// in sync with their generated Go examples.
//
// The Go template cannot hold a real go.mod, since it sits under the embedded
// template tree, so it uses go.mod.embed and go.sum. This command keeps those
// identical to examples/simple-go/go.mod and go.sum. It does not parse versions
// and does not run go commands.
// A Go template cannot hold a real go.mod, since it sits under the embedded
// template tree, so it uses go.mod.embed and go.sum. This command keeps each of
// those identical to its generated example's go.mod and go.sum. It does not
// parse versions and does not run go commands.
//
// Usage:
//
Expand All @@ -30,6 +30,8 @@ type syncPair struct {
var pairs = []syncPair{
{src: "examples/simple-go/go.mod", dst: "templates/go/go.mod.embed"},
{src: "examples/simple-go/go.sum", dst: "templates/go/go.sum"},
{src: "examples/use-cases/scheduled/go/go.mod", dst: "templates/use-cases/scheduled/go/go.mod.embed"},
{src: "examples/use-cases/scheduled/go/go.sum", dst: "templates/use-cases/scheduled/go/go.sum"},
}

func main() {
Expand Down
3 changes: 3 additions & 0 deletions examples/use-cases/scheduled/go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
certs
config
.env
24 changes: 24 additions & 0 deletions examples/use-cases/scheduled/go/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM golang:1.26-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN go build -o worker ./cmd/worker

FROM alpine:latest

WORKDIR /app

# Copy the binary from builder
COPY --from=builder /app/worker .

CMD ["./worker"]
53 changes: 53 additions & 0 deletions examples/use-cases/scheduled/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Hatchet Scheduled Workflow Example

This is an example project demonstrating a scheduled Hatchet workflow in Go. The
worker registers the task with a cron schedule using `WithWorkflowCron`, so it
runs every five minutes. You can also run it on demand. For detailed setup
instructions, see the [Hatchet Setup Guide](https://docs.hatchet.run/home/setup).

## Prerequisites

Before running this project, make sure you have the following:

1. [Go v1.22 or higher](https://go.dev/doc/install)

## Setup

1. Clone the repository:

```bash
git clone https://github.com/hatchet-dev/hatchet-go-quickstart.git
cd hatchet-go-quickstart
```

2. Set the required environment variable `HATCHET_CLIENT_TOKEN` created in the [Getting Started Guide](https://docs.hatchet.run/home/hatchet-cloud-quickstart).

```bash
export HATCHET_CLIENT_TOKEN=<token>
```

> Note: If you're self hosting you may need to set `HATCHET_CLIENT_TLS_STRATEGY=none` to disable TLS

3. Install the project dependencies:

```bash
go mod tidy
```

### Running an example

1. Start a Hatchet worker:

```bash
go run cmd/worker/main.go
```

The worker runs the task on its cron schedule while it is connected.

2. To run the task on demand, in a new terminal:

```bash
go run cmd/run/main.go
```

This triggers the task on the worker running in the first terminal and prints the output to the second terminal.
18 changes: 18 additions & 0 deletions examples/use-cases/scheduled/go/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package client

import (
"errors"
"os"

hatchet "github.com/hatchet-dev/hatchet/sdks/go"
)

func HatchetClient() (*hatchet.Client, error) {
// check for HATCHET_CLIENT_TOKEN
token := os.Getenv("HATCHET_CLIENT_TOKEN")
if token == "" {
return nil, errors.New("HATCHET_CLIENT_TOKEN is not set")
}

return hatchet.NewClient()
}
35 changes: 35 additions & 0 deletions examples/use-cases/scheduled/go/cmd/run/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"context"
"fmt"
"log"

"github.com/hatchet-dev/hatchet-go-quickstart/client"
"github.com/hatchet-dev/hatchet-go-quickstart/workflows"
)

func main() {
c, err := client.HatchetClient()
if err != nil {
log.Fatalf("Failed to create Hatchet client: %v", err)
}

scheduled := workflows.ScheduledWorkflow(c)

result, err := scheduled.Run(context.Background(), workflows.ScheduledInput{
Message: "hello from a manual run",
})
if err != nil {
log.Fatalf("Failed to run Hatchet task: %v", err)
}

var scheduledResult workflows.ScheduledOutput

err = result.Into(&scheduledResult)
if err != nil {
log.Fatalf("Failed to convert result to ScheduledOutput: %v", err)
}

fmt.Println(scheduledResult.Message)
}
36 changes: 36 additions & 0 deletions examples/use-cases/scheduled/go/cmd/worker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"log"

"github.com/hatchet-dev/hatchet-go-quickstart/client"
"github.com/hatchet-dev/hatchet-go-quickstart/workflows"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
hatchet "github.com/hatchet-dev/hatchet/sdks/go"
)

func main() {
c, err := client.HatchetClient()
if err != nil {
log.Fatalf("Failed to create Hatchet client: %v", err)
}

worker, err := c.NewWorker(
"scheduled-worker",
hatchet.WithWorkflows(workflows.ScheduledWorkflow(c)),
)
if err != nil {
log.Fatalf("Failed to create Hatchet worker: %v", err)
}

// we construct an interrupt context to handle Ctrl+C
// you can pass in your own context.Context here to the worker
interruptCtx, cancel := cmdutils.NewInterruptContext()

defer cancel()

err = worker.StartBlocking(interruptCtx)
if err != nil {
log.Fatalf("Failed to start Hatchet worker: %v", err)
}
}
Loading