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
39 changes: 39 additions & 0 deletions .github/workflows/examples-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: examples-check

# Generated example install, build, and typecheck checks. This workflow does not
# run workers or reach a Hatchet engine; runtime behavior is covered by
# examples-e2e. Kept separate from the validate workflow because it installs
# three language toolchains.
on:
pull_request:
push:
branches:
- main

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

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

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"

- name: Install Poetry
run: pipx install poetry

- name: Install pnpm
run: npm install -g pnpm@10

- name: Run example checks
run: bash hack/check-examples.sh
56 changes: 56 additions & 0 deletions .github/workflows/examples-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: examples-e2e

# Runtime end-to-end test for the generated examples. It runs each generated
# worker against a real engine and triggers its workflow, proving the examples
# run rather than only compile (the examples-check workflow). It needs the
# Hatchet CLI and Docker, so it is heavier and kept separate.
on:
workflow_dispatch:
inputs:
server_tag:
description: "hatchet-lite image tag to test against"
default: "latest"
required: false
pull_request:
push:
branches:
- main

env:
SERVER_TAG: ${{ github.event.inputs.server_tag || 'latest' }}

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

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

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"

- name: Install Poetry
run: pipx install poetry

- name: Install pnpm
run: npm install -g pnpm@10

- name: Install Hatchet CLI
run: |
curl -fsSL https://install.hatchet.run/install.sh | bash
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Verify Hatchet CLI
run: hatchet --help

- name: Run example e2e
run: HATCHET_E2E_LANGUAGES="go python typescript" HATCHET_SERVER_TAG="$SERVER_TAG" bash hack/e2e-examples.sh
48 changes: 48 additions & 0 deletions hack/check-examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Generated example install, build, and typecheck checks.
#
# Go builds and vets, TypeScript installs and typechecks against its real
# dependencies, and Python installs its dependencies and byte-compiles its
# sources. Byte-compile checks Python syntax only, not that the example uses the
# SDK correctly. It does not run workers or reach a Hatchet engine; runtime
# behavior is covered by hack/e2e-examples.sh.
#
# Run from the repository root. Requires go, node with pnpm, and python with
# poetry on PATH.
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"

# Remove gitignored build artifacts these checks create under the examples, so a
# later `go run ./cmd/generate-examples --check` is not tripped by them.
clean_example_artifacts() {
rm -rf examples/simple-python/.venv \
examples/simple-typescript/node_modules \
examples/simple-typescript/dist
find examples/simple-python -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
}
trap clean_example_artifacts EXIT

echo "== Go example: build and vet =="
(
cd examples/simple-go
go build ./...
go vet ./...
)

echo "== TypeScript example: install and typecheck =="
(
cd examples/simple-typescript
pnpm install --frozen-lockfile
pnpm exec tsc --noEmit
)

echo "== Python example: install and byte-compile =="
(
cd examples/simple-python
poetry install --no-interaction
poetry run python -m compileall -q src
)

echo "All example checks passed."
147 changes: 147 additions & 0 deletions hack/e2e-examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env bash
# Runtime end-to-end test for the generated examples.
#
# It starts one local Hatchet engine, then for each selected language starts the
# generated worker, triggers the workflow, and asserts the trigger exits zero
# with the expected output. Workers and containers are removed on exit.
#
# Readiness comes from the trigger itself: the worker is started, then the
# trigger is retried within a bounded window until it succeeds or the worker
# process exits. This avoids "hatchet worker list", which needs a TTY, and
# avoids guessing language-specific readiness log lines.
#
# Run from the repository root. Requires hatchet and docker on PATH, plus the
# toolchain each selected example needs (go, poetry, or node with pnpm).
#
# Environment:
# HATCHET_SERVER_TAG hatchet-lite image tag (default "latest")
# HATCHET_E2E_LANGUAGES space-separated subset of "go python typescript"
# (default all three)
set -euo pipefail

SERVER_TAG="${HATCHET_SERVER_TAG:-latest}"
LANGUAGES="${HATCHET_E2E_LANGUAGES:-go python typescript}"

# Bounded timing. A single trigger attempt is capped, and the per-language wait
# is capped, so a stuck worker or engine cannot hang the run.
TRIGGER_TIMEOUT="${HATCHET_E2E_TRIGGER_TIMEOUT:-180}"
MAX_WAIT="${HATCHET_E2E_MAX_WAIT:-420}"
RETRY_INTERVAL="${HATCHET_E2E_RETRY_INTERVAL:-5}"

COMPOSE_PROJECT="hatchet-cli"
COMPOSE_NETWORK="hatchet-cli_default"

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"

LOGDIR="$(mktemp -d)"
WORKER_PIDS=()

# kill_tree terminates a specific process and its descendants. It is scoped to
# one pid, never a broad pattern match.
kill_tree() {
local pid="$1" child
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
kill_tree "$child"
done
kill -TERM "$pid" 2>/dev/null || true
}

# clean_example_artifacts removes gitignored build artifacts the workers create
# under the examples (poetry .venv, __pycache__, pnpm node_modules), so a later
# `go run ./cmd/generate-examples --check` is not tripped by them.
clean_example_artifacts() {
rm -rf examples/simple-python/.venv \
examples/simple-typescript/node_modules \
examples/simple-typescript/dist
find examples/simple-python -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
}

cleanup() {
if [ "${#WORKER_PIDS[@]}" -gt 0 ]; then
for pid in "${WORKER_PIDS[@]}"; do
kill_tree "$pid"
done
fi
docker ps -aq --filter "label=com.docker.compose.project=${COMPOSE_PROJECT}" \
| xargs -r docker rm -f >/dev/null 2>&1 || true
docker network rm "${COMPOSE_NETWORK}" >/dev/null 2>&1 || true
clean_example_artifacts
rm -rf "$LOGDIR"
}
trap cleanup EXIT

require() {
command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' is required on PATH" >&2; exit 2; }
}

require hatchet
require docker
docker info >/dev/null 2>&1 || { echo "ERROR: docker daemon is not available" >&2; exit 2; }

run_language() {
local lang="$1" dir="$2"; shift 2
local expected=("$@")
local logf="$LOGDIR/${lang}-worker.log"
local trigf="$LOGDIR/${lang}-trigger.log"

echo "== ${lang}: starting worker =="
( cd "$dir" && exec hatchet worker dev --profile local --no-reload ) >"$logf" 2>&1 &
local wpid=$!
WORKER_PIDS+=("$wpid")

echo "== ${lang}: triggering until ready (max ${MAX_WAIT}s) =="
local deadline=$(( $(date +%s) + MAX_WAIT ))
local ok=0
while [ "$(date +%s)" -lt "$deadline" ]; do
if ! kill -0 "$wpid" 2>/dev/null; then
echo "ERROR: ${lang} worker exited before the trigger succeeded" >&2
echo "--- last 40 lines of ${lang} worker log ---" >&2
tail -40 "$logf" >&2 || true
return 1
fi
if ( cd "$dir" && timeout "$TRIGGER_TIMEOUT" hatchet trigger simple --profile local ) >"$trigf" 2>&1; then
ok=1
break
fi
sleep "$RETRY_INTERVAL"
done

if [ "$ok" -ne 1 ]; then
echo "ERROR: ${lang} trigger did not succeed within ${MAX_WAIT}s" >&2
echo "--- last 40 lines of ${lang} trigger log ---" >&2
tail -40 "$trigf" >&2 || true
return 1
fi

local missing=0 exp
for exp in "${expected[@]}"; do
if ! grep -qiF "$exp" "$trigf"; then
echo "ERROR: ${lang} trigger did not print expected output: ${exp}" >&2
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "--- ${lang} trigger output ---" >&2
cat "$trigf" >&2 || true
return 1
fi

echo "== ${lang}: PASS =="
kill_tree "$wpid"
return 0
}

echo "Starting local Hatchet runtime (tag: ${SERVER_TAG})"
hatchet server start --profile local --tag "$SERVER_TAG"

for lang in $LANGUAGES; do
case "$lang" in
go) run_language go examples/simple-go "hello, world!" ;;
python) run_language python examples/simple-python "42" ;;
typescript) run_language typescript examples/simple-typescript "Hello, world!" "Hello, moon!" ;;
*) echo "ERROR: unknown language '${lang}'" >&2; exit 2 ;;
esac
done

echo "All requested example e2e checks passed: ${LANGUAGES}"