Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions examples/use-cases/scheduled/go/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
HATCHET_CLIENT_TOKEN=
HATCHET_CLIENT_TLS_STRATEGY=none
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"]
55 changes: 55 additions & 0 deletions examples/use-cases/scheduled/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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

You can also use a `.env` file to load these environment variables. A sample exists in the form of a `.env.sample`.

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.
26 changes: 26 additions & 0 deletions examples/use-cases/scheduled/go/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package client

import (
"errors"
"os"

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

func HatchetClient() (*hatchet.Client, error) {
if _, err := os.Stat(".env"); os.IsExist(err) {
err := godotenv.Load()
if err != nil {
return nil, err
}
}
Comment thread
BloggerBust marked this conversation as resolved.
Outdated

// 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