Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
4fa5a92
initial commit
juliusgeo May 7, 2026
ff8e492
add for other languages
juliusgeo May 7, 2026
a0a5a1f
Merge branch 'main' into e2e_go_dockerfile
juliusgeo May 11, 2026
e2edf82
remove test running from image, will be done elsewhere
juliusgeo May 12, 2026
dac3be7
ok I guess I need those
juliusgeo May 12, 2026
4f87494
try to add a handler for killing ruby at end of tests
juliusgeo May 12, 2026
179dfcc
try this to make the tests exit
juliusgeo May 12, 2026
f422c5e
try fix for go e2e tests
juliusgeo May 12, 2026
c6dec0b
try timeout
juliusgeo May 12, 2026
d6daef6
Merge branch 'main' into e2e_go_dockerfile
juliusgeo May 12, 2026
6d2803f
regenerate lock
juliusgeo May 12, 2026
163c1ef
use durable context
juliusgeo May 12, 2026
a135f71
rename all workflows so they don't conflict
juliusgeo May 12, 2026
0466ab9
rename event keys
juliusgeo May 12, 2026
719963e
update other sdks
juliusgeo May 12, 2026
653043c
some fixes
juliusgeo May 12, 2026
465f534
remove some uneeded stuff
juliusgeo May 12, 2026
e6797cb
rename some more of 'em
juliusgeo May 13, 2026
cd3bb3a
change up entrypoint
juliusgeo May 13, 2026
1f0e11d
more namespacing
juliusgeo May 13, 2026
f9b5a79
even more namespacing
juliusgeo May 13, 2026
a20b409
fix go tests
juliusgeo May 13, 2026
97a19f9
fix a small issue
juliusgeo May 13, 2026
1057b0d
more fixes
juliusgeo May 13, 2026
1d9d7ec
try this
juliusgeo May 13, 2026
5d26848
revert bad namespacing
juliusgeo May 13, 2026
5fafd74
use actual namespaces :/
juliusgeo May 13, 2026
6487c3e
see if this fixes ruby
juliusgeo May 13, 2026
dc0668a
try this
juliusgeo May 13, 2026
8df6474
fix for go e2e tests
juliusgeo May 13, 2026
256a464
try this for python
juliusgeo May 13, 2026
8175427
rename keys and such
juliusgeo May 13, 2026
54be53e
try this for ruby
juliusgeo May 13, 2026
464f5a4
try this :/
juliusgeo May 13, 2026
9fe397f
for python as well
juliusgeo May 13, 2026
9988bbd
use env vars instead for controlling namespace
juliusgeo May 13, 2026
53a0dd6
clean up
juliusgeo May 13, 2026
1317713
try this for python
juliusgeo May 14, 2026
7cc9506
try this for python
juliusgeo May 14, 2026
8745b51
missed a bunch
juliusgeo May 14, 2026
361f7ed
fix this lol
juliusgeo May 14, 2026
51fd499
harden test
juliusgeo May 14, 2026
9916fba
increase sleep to make less flaky
juliusgeo May 14, 2026
2d376db
try this for fixing typescript test
juliusgeo May 14, 2026
6c9ae1c
see if this fixes ruby in CI
juliusgeo May 14, 2026
3b10fb2
nevermind
juliusgeo May 14, 2026
6cdf262
revert a change
juliusgeo May 14, 2026
3ad0596
retry logic for python tests
juliusgeo May 14, 2026
1d1cb6f
strengthen another python test
juliusgeo May 14, 2026
c945859
change up ruby entrypoint script
juliusgeo May 14, 2026
1d45dc3
fix up ts tests
juliusgeo May 14, 2026
a28c7f4
make ruby test less flaky
juliusgeo May 14, 2026
5354ee8
lint python
juliusgeo May 14, 2026
203812e
adjust go comparison
juliusgeo May 14, 2026
bed6a3a
make go tests less flaky
juliusgeo May 14, 2026
6dc90ed
add tolerance to go tests
juliusgeo May 14, 2026
0d3d19c
fix another
juliusgeo May 14, 2026
56211e5
a bunch of changes
juliusgeo May 14, 2026
8ea02c9
eurgh
juliusgeo May 14, 2026
a3604ae
lint fixes
juliusgeo May 14, 2026
b6d2d81
even MORE refactors for python tests
juliusgeo May 14, 2026
c2a4086
fix retries
juliusgeo May 14, 2026
d9067c4
fix weird bug with asyncsession
juliusgeo May 15, 2026
16a8a17
lengthen timeouts on evictable tasks
juliusgeo May 15, 2026
be98207
more fixes for typescript
juliusgeo May 15, 2026
59c0eef
even more bumping of timeouts
juliusgeo May 15, 2026
a8f6f35
more typescript fixes and python fixes
juliusgeo May 15, 2026
3380c92
more typescript fixes and python fixes
juliusgeo May 15, 2026
1eb729d
try to make ruby tests a bit more resilient
juliusgeo May 15, 2026
367fe8b
lint etc
juliusgeo May 15, 2026
ef0607a
fix bulk replay for good thing time I think
juliusgeo May 15, 2026
ab28aa8
some more fixes for ts and python
juliusgeo May 18, 2026
8494ce4
Merge branch 'main' into e2e_go_dockerfile
juliusgeo May 18, 2026
d944b36
add poll to simple worker
juliusgeo May 18, 2026
13e0ca4
small cleanup
juliusgeo May 19, 2026
e73a790
fix lint
juliusgeo May 19, 2026
a339d41
remove on push e2e build
juliusgeo May 19, 2026
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
83 changes: 83 additions & 0 deletions .github/workflows/e2e-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: e2e-image
on:
push:
branches:
- main
paths:
- "sdks/go/**"
- "sdks/python/**"
- "sdks/typescript/**"
- "sdks/ruby/**"

jobs:
build-push-e2e-go:
name: hatchet-e2e-go
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push
run: |
DOCKER_BUILDKIT=1 docker build -f ./build/package/e2e-go.dockerfile \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-go:${{ github.sha }} \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-go:main \
--platform linux/amd64 \
.
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-go:${{ github.sha }}
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-go:main

build-push-e2e-python:
name: hatchet-e2e-python
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push
run: |
DOCKER_BUILDKIT=1 docker build -f ./build/package/e2e-python.dockerfile \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-python:${{ github.sha }} \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-python:main \
--platform linux/amd64 \
.
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-python:${{ github.sha }}
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-python:main

build-push-e2e-typescript:
name: hatchet-e2e-typescript
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push
run: |
DOCKER_BUILDKIT=1 docker build -f ./build/package/e2e-typescript.dockerfile \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-typescript:${{ github.sha }} \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-typescript:main \
--platform linux/amd64 \
.
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-typescript:${{ github.sha }}
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-typescript:main

build-push-e2e-ruby:
name: hatchet-e2e-ruby
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push
run: |
DOCKER_BUILDKIT=1 docker build -f ./build/package/e2e-ruby.dockerfile \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-ruby:${{ github.sha }} \
-t ghcr.io/hatchet-dev/hatchet/hatchet-e2e-ruby:main \
--platform linux/amd64 \
.
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-ruby:${{ github.sha }}
docker push ghcr.io/hatchet-dev/hatchet/hatchet-e2e-ruby:main
2 changes: 1 addition & 1 deletion .github/workflows/sdk-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
- name: Run pytest
run: |
echo "Using HATCHET_CLIENT_NAMESPACE: $HATCHET_CLIENT_NAMESPACE"

export HATCHET_CLIENT_SERVER_URL=http://localhost:8080
poetry run pytest -s -vvv --maxfail=5 --capture=no --retries 3 --retry-delay 2 -n 8

- name: Test with wheel
Expand Down
31 changes: 31 additions & 0 deletions build/package/e2e-go.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Base Go environment
# -------------------
FROM golang:1.25-alpine as base
WORKDIR /hatchet

COPY go.mod go.sum ./

RUN go mod download

COPY /pkg ./pkg
COPY /internal ./internal
COPY /api ./api
COPY /sdks/go ./sdks/go

# Go build environment
# --------------------
FROM base AS build-go

RUN go test -c -tags e2e -v -o ./bin/e2e-test ./sdks/go/e2e/

# Deployment environment
# ----------------------
FROM alpine AS deployment

WORKDIR /hatchet

RUN apk update && apk add --no-cache ca-certificates tzdata

COPY --from=build-go /hatchet/bin/e2e-test /hatchet/

CMD ["/hatchet/e2e-test", "-test.v", "-test.timeout=10m"]
13 changes: 13 additions & 0 deletions build/package/e2e-python.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Base Python environment
# -----------------------
FROM python:3.13-slim AS deployment

WORKDIR /hatchet/sdks/python

RUN pip install --no-cache-dir poetry==2.3.0

COPY sdks/python/ .

RUN poetry install --no-interaction --all-extras

CMD ["poetry", "run", "pytest", "-s", "-vvv", "-x", "--capture=no"]
44 changes: 44 additions & 0 deletions build/package/e2e-ruby-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh

WORKER_PID=""
FINAL_EXIT=0

cleanup() {
if [ -n "$WORKER_PID" ]; then
kill -9 "$WORKER_PID" 2>/dev/null || true
wait "$WORKER_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM

echo "=== Running integration tests ==="
cd /hatchet/sdks/ruby/src
timeout 300 bundle exec rspec spec/integration/ --format documentation --tag integration
INTEGRATION_EXIT=$?
[ $INTEGRATION_EXIT -ne 0 ] && FINAL_EXIT=$INTEGRATION_EXIT

echo "=== Starting example worker ==="
cd /hatchet/sdks/ruby/examples
HATCHET_CLIENT_WORKER_HEALTHCHECK_ENABLED=true \
HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT=8001 \
bundle exec ruby worker.rb &
WORKER_PID=$!

for i in $(seq 1 60); do
if curl -sf http://localhost:8001/health > /dev/null 2>&1; then
echo "Worker healthy after ${i}s"
break
fi
if [ "$i" -eq 60 ]; then
echo "Worker failed to start within 60s"
exit 1
fi
sleep 1
done

echo "=== Running e2e tests ==="
timeout 1200 bundle exec rspec -f d --fail-fast
E2E_EXIT=$?
[ $E2E_EXIT -ne 0 ] && FINAL_EXIT=$E2E_EXIT

exit $FINAL_EXIT
18 changes: 18 additions & 0 deletions build/package/e2e-ruby.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Base Ruby environment
# ---------------------
FROM ruby:3.2 AS deployment

WORKDIR /hatchet/sdks/ruby

RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*

COPY sdks/ruby/src/ src/
RUN cd src && bundle lock --add-platform x86_64-linux && bundle install

COPY sdks/ruby/examples/ examples/
RUN cd examples && bundle lock --add-platform x86_64-linux && bundle install

COPY build/package/e2e-ruby-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"]
13 changes: 13 additions & 0 deletions build/package/e2e-typescript.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Base Node environment
# ---------------------
FROM node:20-alpine AS deployment

WORKDIR /hatchet/sdks/typescript

RUN corepack enable && corepack prepare pnpm@10.16.1 --activate

COPY sdks/typescript/ .

RUN pnpm install --frozen-lockfile

CMD ["pnpm", "run", "test:e2e"]
2 changes: 1 addition & 1 deletion examples/python/durable_eviction/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async def capacity_evictable_sleep(
)
async def non_evictable_sleep(input: EmptyModel, ctx: DurableContext) -> dict[str, Any]:
"""Has eviction disabled -- should never be evicted."""
await ctx.aio_sleep_for(timedelta(seconds=10))
await ctx.aio_sleep_for(timedelta(seconds=30))
return {"status": "completed"}


Expand Down
3 changes: 3 additions & 0 deletions pkg/worker/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/client/create"
"github.com/hatchet-dev/hatchet/pkg/client/types"
clientconfig "github.com/hatchet-dev/hatchet/pkg/config/client"
"github.com/hatchet-dev/hatchet/pkg/worker/condition"
)

Expand Down Expand Up @@ -1061,6 +1062,8 @@ func (d *durableHatchetContext) SleepFor(duration time.Duration) (*SingleWaitRes

// WaitForEvent implements the DurableHatchetContext.WaitForEvent method.
func (d *durableHatchetContext) WaitForEvent(eventKey, expression string) (*SingleWaitResult, error) {
namespace := d.c.Namespace()
eventKey = clientconfig.ApplyNamespace(eventKey, &namespace)
wr, err := d.waitFor(condition.UserEventCondition(eventKey, expression), "wait_for_event", eventKey)

if err != nil {
Expand Down
37 changes: 24 additions & 13 deletions sdks/go/e2e/durable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func TestDurableWorkflow(t *testing.T) {

id := uniqueID()

time.Sleep(time.Duration(sleepTime+10) * time.Second)
// Wait for the run to start, then let the internal SleepFor(sleepTime) finish before pushing the event.
pollUntilRunStatus(t, ctx, sharedClient, ref.RunId, string(rest.V1TaskStatusRUNNING))
time.Sleep(time.Duration(sleepTime+3) * time.Second)

err = sharedClient.Events().Push(ctx, eventKey, AwaitedEvent{ID: id})
require.NoError(t, err)
Expand All @@ -68,15 +70,21 @@ func TestDurableSleepCancelReplay(t *testing.T) {
ref, err := testWaitForSleepTwice.RunNoWait(ctx, EmptyInput{})
require.NoError(t, err)

time.Sleep(time.Duration(sleepTime/2) * time.Second)
pollUntilRunStatus(t, ctx, sharedClient, ref.RunId, string(rest.V1TaskStatusRUNNING))

_, err = sharedClient.Runs().Cancel(ctx, rest.V1CancelTaskRequest{
ExternalIds: toUUIDs(ref.RunId),
})
require.NoError(t, err)

// Wait for cancellation
time.Sleep(2 * time.Second)
// Wait for cancellation to propagate before replaying.
pollUntil(t, ctx, func() (bool, error) {
status, err := sharedClient.Runs().GetStatus(ctx, ref.RunId)
if err != nil {
return false, err
}
return *status == rest.V1TaskStatusCANCELLED, nil
})

replayStart := time.Now()
_, err = sharedClient.Runs().Replay(ctx, rest.V1ReplayTaskRequest{
Expand All @@ -92,8 +100,8 @@ func TestDurableSleepCancelReplay(t *testing.T) {
err = result.TaskOutput("wait-for-sleep-twice").Into(&output)
require.NoError(t, err)

assert.Less(t, output["runtime"], float64(sleepTime))
assert.LessOrEqual(t, replayElapsed, float64(sleepTime))
assert.Less(t, output["runtime"], float64(sleepTime)+timingTolerance)
assert.LessOrEqual(t, replayElapsed, float64(sleepTime)+timingTolerance)
}

func TestDurableChildSpawn(t *testing.T) {
Expand Down Expand Up @@ -122,6 +130,7 @@ func TestDurableChildBulkSpawn(t *testing.T) {
require.NoError(t, err)
outputs, ok := m["child_outputs"].([]any)
require.True(t, ok, "expected child_outputs to be an array")

assert.GreaterOrEqual(t, len(outputs), n-1)
assert.LessOrEqual(t, len(outputs), n)

Expand All @@ -145,7 +154,9 @@ func TestDurableSleepEventSpawnReplay(t *testing.T) {
ref, err := testDurableSleepEventSpawn.RunNoWait(ctx, EmptyInput{})
require.NoError(t, err)

time.Sleep(time.Duration(sleepTime+5) * time.Second)
// Wait for the run to start, then let the internal SleepFor(sleepTime) finish before pushing the event.
pollUntilRunStatus(t, ctx, sharedClient, ref.RunId, string(rest.V1TaskStatusRUNNING))
time.Sleep(time.Duration(sleepTime+3) * time.Second)
err = sharedClient.Events().Push(ctx, eventKey, map[string]string{"test": "test"})
require.NoError(t, err)

Expand Down Expand Up @@ -173,7 +184,7 @@ func TestDurableSleepEventSpawnReplay(t *testing.T) {
replayChild, ok := rm["child_output"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "hello from child 1", replayChild["message"])
assert.Less(t, replayElapsed, float64(sleepTime))
assert.Less(t, replayElapsed, float64(sleepTime)+timingTolerance)
}

func TestDurableCompletedReplay(t *testing.T) {
Expand Down Expand Up @@ -207,8 +218,8 @@ func TestDurableCompletedReplay(t *testing.T) {
var replayOutput map[string]float64
err = replayResult.TaskOutput("wait-for-sleep-twice").Into(&replayOutput)
require.NoError(t, err)
assert.Less(t, replayOutput["runtime"], float64(sleepTime))
assert.Less(t, elapsed, float64(sleepTime))
assert.Less(t, replayOutput["runtime"], float64(sleepTime)+timingTolerance)
assert.Less(t, elapsed, float64(sleepTime)+timingTolerance)
}

func TestDurableSpawnDAG(t *testing.T) {
Expand Down Expand Up @@ -306,7 +317,7 @@ func TestDurableReplayReset(t *testing.T) {
durations := []float64{resetOutput.Sleep1Duration, resetOutput.Sleep2Duration, resetOutput.Sleep3Duration}
for i, d := range durations {
if int64(i+1) < nodeID {
assert.Less(t, d, float64(replayResetSleepTime))
assert.Less(t, d, float64(replayResetSleepTime)+timingTolerance)
} else {
assert.GreaterOrEqual(t, d, float64(replayResetSleepTime))
}
Expand Down Expand Up @@ -369,7 +380,7 @@ func TestDurableBranchingOffBranch(t *testing.T) {
err = resetResult2.TaskOutput("durable-replay-reset").Into(&resetOutput2)
require.NoError(t, err)

assert.Less(t, resetOutput2.Sleep1Duration, float64(replayResetSleepTime))
assert.Less(t, resetOutput2.Sleep1Duration, float64(replayResetSleepTime)+timingTolerance)
assert.GreaterOrEqual(t, resetOutput2.Sleep2Duration, float64(replayResetSleepTime))
assert.GreaterOrEqual(t, resetOutput2.Sleep3Duration, float64(replayResetSleepTime))
assert.GreaterOrEqual(t, resetElapsed2, float64(2*replayResetSleepTime))
Expand Down Expand Up @@ -407,7 +418,7 @@ func TestDurableMemoizationViaReplay(t *testing.T) {
require.NoError(t, err)

assert.GreaterOrEqual(t, duration1, float64(sleepTime))
assert.Less(t, duration2, 1.0)
assert.Less(t, duration2, 1.1)
assert.Equal(t, output1.Message, output2.Message)
}

Expand Down
13 changes: 6 additions & 7 deletions sdks/go/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import (
)

const (
defaultTimeout = 5 * time.Minute
pollInterval = 200 * time.Millisecond
maxPolls = 150
defaultTimeout = 5 * time.Minute
pollInterval = 200 * time.Millisecond
timingTolerance = 1.0 // seconds of slack for scheduling/measurement overhead in timing assertions
)

var (
Expand Down Expand Up @@ -62,13 +62,13 @@ func uniqueID() string {
return uuid.New().String()
}

// pollUntil polls fn every pollInterval until it returns true or maxPolls is reached.
// pollUntil polls fn every pollInterval until it returns true or the context is done.
func pollUntil(t *testing.T, ctx context.Context, fn func() (bool, error)) {
t.Helper()
for i := 0; i < maxPolls; i++ {
for {
done, err := fn()
if err != nil {
t.Logf("poll error (attempt %d): %v", i, err)
t.Logf("poll error: %v", err)
}
if done {
return
Expand All @@ -79,7 +79,6 @@ func pollUntil(t *testing.T, ctx context.Context, fn func() (bool, error)) {
case <-time.After(pollInterval):
}
}
t.Fatalf("polling timed out after %d attempts", maxPolls)
}

// pollUntilRunStatus polls run details until any task reaches the given status.
Expand Down
Loading
Loading