From 4fa9cce85726a210225d2b488505ea56bd431bc9 Mon Sep 17 00:00:00 2001 From: kingwill101 Date: Tue, 24 Feb 2026 08:11:57 -0500 Subject: [PATCH 1/4] feat(stem): add URL bootstrap wiring and taskfile-based examples --- README.md | 49 ++-- Taskfile.yml | 84 +++++- packages/dashboard/CHANGELOG.md | 3 + packages/stem/CHANGELOG.md | 13 +- packages/stem/README.md | 46 ++- packages/stem/Taskfile.yml | 5 + packages/stem/example/Taskfile.yml | 188 +++++++++++++ .../stem/example/autoscaling_demo/README.md | 10 +- .../example/autoscaling_demo/Taskfile.yml | 81 ++++++ .../stem/example/autoscaling_demo/justfile | 43 --- .../stem/example/canvas_patterns/Taskfile.yml | 59 ++++ .../stem/example/canvas_patterns/justfile | 33 --- .../example/daemonized_worker/Taskfile.yml | 31 ++ .../stem/example/daemonized_worker/justfile | 27 -- packages/stem/example/dlq_sandbox/README.md | 12 +- .../stem/example/dlq_sandbox/Taskfile.yml | 70 +++++ packages/stem/example/dlq_sandbox/justfile | 43 --- .../example/docs_snippets/lib/signals.dart | 5 +- .../example/docs_snippets/lib/uniqueness.dart | 9 +- .../example/docs_snippets/lib/workflows.dart | 18 +- .../stem/example/encrypted_payload/README.md | 15 +- .../example/encrypted_payload/Taskfile.yml | 170 +++++++++++ .../stem/example/encrypted_payload/justfile | 41 --- packages/stem/example/microservice/README.md | 29 +- .../stem/example/microservice/Taskfile.yml | 266 ++++++++++++++++++ packages/stem/example/microservice/justfile | 60 ---- .../example/monolith_service/Taskfile.yml | 26 ++ .../stem/example/monolith_service/justfile | 24 -- .../stem/example/ops_health_suite/README.md | 22 +- .../example/ops_health_suite/Taskfile.yml | 70 +++++ .../stem/example/ops_health_suite/justfile | 43 --- packages/stem/example/postgres_tls/README.md | 28 +- .../stem/example/postgres_tls/Taskfile.yml | 107 +++++++ .../example/postgres_tls/docker-compose.yml | 45 +++ packages/stem/example/postgres_tls/justfile | 35 --- .../stem/example/progress_heartbeat/README.md | 18 +- .../example/progress_heartbeat/Taskfile.yml | 71 +++++ .../stem/example/progress_heartbeat/justfile | 43 --- .../stem/example/rate_limit_delay/README.md | 12 +- .../example/rate_limit_delay/Taskfile.yml | 71 +++++ .../stem/example/rate_limit_delay/justfile | 43 --- packages/stem/example/retry_task/README.md | 12 +- packages/stem/example/retry_task/Taskfile.yml | 68 +++++ packages/stem/example/retry_task/justfile | 43 --- .../example/security/ed25519_tls/README.md | 19 +- .../example/security/ed25519_tls/Taskfile.yml | 86 ++++++ .../example/security/ed25519_tls/justfile | 28 -- packages/stem/example/security/hmac/README.md | 13 +- .../stem/example/security/hmac/Taskfile.yml | 74 +++++ packages/stem/example/security/hmac/justfile | 28 -- .../stem/example/security/hmac_tls/README.md | 16 +- .../example/security/hmac_tls/Taskfile.yml | 86 ++++++ .../stem/example/security/hmac_tls/justfile | 28 -- packages/stem/example/signals_demo/README.md | 12 +- .../stem/example/signals_demo/Taskfile.yml | 68 +++++ packages/stem/example/signals_demo/justfile | 43 --- .../example/signing_key_rotation/README.md | 13 +- .../example/signing_key_rotation/Taskfile.yml | 152 ++++++++++ .../example/signing_key_rotation/justfile | 49 ---- packages/stem/example/stack_autowire.dart | 32 +-- .../stem/example/task_context_mixed/README.md | 12 +- .../example/task_context_mixed/Taskfile.yml | 101 +++++++ .../stem/example/task_context_mixed/justfile | 53 ---- .../stem/example/unique_tasks/Taskfile.yml | 26 ++ packages/stem/example/unique_tasks/justfile | 25 -- .../stem/example/worker_control_lab/README.md | 28 +- .../example/worker_control_lab/Taskfile.yml | 73 +++++ .../stem/example/worker_control_lab/justfile | 44 --- packages/stem/example/workflows/Taskfile.yml | 53 ++++ .../example/workflows/custom_factories.dart | 11 +- packages/stem/example/workflows/justfile | 42 --- .../stem/example/workflows/sqlite_store.dart | 5 +- packages/stem/lib/src/bootstrap/stem_app.dart | 102 +++++++ .../stem/lib/src/bootstrap/stem_client.dart | 47 ++++ .../stem/lib/src/bootstrap/stem_stack.dart | 60 +++- .../stem/lib/src/bootstrap/workflow_app.dart | 74 +++++ .../stem/test/bootstrap/stem_app_test.dart | 149 ++++++++++ .../stem/test/bootstrap/stem_client_test.dart | 70 +++++ .../stem/test/bootstrap/stem_stack_test.dart | 124 +++++++- packages/stem_adapter_tests/CHANGELOG.md | 4 +- packages/stem_adapter_tests/README.md | 126 +++++++-- packages/stem_builder/CHANGELOG.md | 2 +- packages/stem_cli/CHANGELOG.md | 6 +- packages/stem_memory/CHANGELOG.md | 6 +- packages/stem_memory/README.md | 28 +- .../stem_memory/lib/src/memory_factories.dart | 2 +- packages/stem_memory/lib/stem_memory.dart | 2 +- packages/stem_postgres/CHANGELOG.md | 6 + .../lib/src/stack/postgres_adapter.dart | 2 +- .../lib/src/workflow/postgres_factories.dart | 6 +- packages/stem_redis/CHANGELOG.md | 2 +- packages/stem_sqlite/CHANGELOG.md | 2 +- 92 files changed, 3152 insertions(+), 1079 deletions(-) create mode 100644 packages/dashboard/CHANGELOG.md create mode 100644 packages/stem/example/Taskfile.yml create mode 100644 packages/stem/example/autoscaling_demo/Taskfile.yml delete mode 100644 packages/stem/example/autoscaling_demo/justfile create mode 100644 packages/stem/example/canvas_patterns/Taskfile.yml delete mode 100644 packages/stem/example/canvas_patterns/justfile create mode 100644 packages/stem/example/daemonized_worker/Taskfile.yml delete mode 100644 packages/stem/example/daemonized_worker/justfile create mode 100644 packages/stem/example/dlq_sandbox/Taskfile.yml delete mode 100644 packages/stem/example/dlq_sandbox/justfile create mode 100644 packages/stem/example/encrypted_payload/Taskfile.yml delete mode 100644 packages/stem/example/encrypted_payload/justfile create mode 100644 packages/stem/example/microservice/Taskfile.yml delete mode 100644 packages/stem/example/microservice/justfile create mode 100644 packages/stem/example/monolith_service/Taskfile.yml delete mode 100644 packages/stem/example/monolith_service/justfile create mode 100644 packages/stem/example/ops_health_suite/Taskfile.yml delete mode 100644 packages/stem/example/ops_health_suite/justfile create mode 100644 packages/stem/example/postgres_tls/Taskfile.yml create mode 100644 packages/stem/example/postgres_tls/docker-compose.yml delete mode 100644 packages/stem/example/postgres_tls/justfile create mode 100644 packages/stem/example/progress_heartbeat/Taskfile.yml delete mode 100644 packages/stem/example/progress_heartbeat/justfile create mode 100644 packages/stem/example/rate_limit_delay/Taskfile.yml delete mode 100644 packages/stem/example/rate_limit_delay/justfile create mode 100644 packages/stem/example/retry_task/Taskfile.yml delete mode 100644 packages/stem/example/retry_task/justfile create mode 100644 packages/stem/example/security/ed25519_tls/Taskfile.yml delete mode 100644 packages/stem/example/security/ed25519_tls/justfile create mode 100644 packages/stem/example/security/hmac/Taskfile.yml delete mode 100644 packages/stem/example/security/hmac/justfile create mode 100644 packages/stem/example/security/hmac_tls/Taskfile.yml delete mode 100644 packages/stem/example/security/hmac_tls/justfile create mode 100644 packages/stem/example/signals_demo/Taskfile.yml delete mode 100644 packages/stem/example/signals_demo/justfile create mode 100644 packages/stem/example/signing_key_rotation/Taskfile.yml delete mode 100644 packages/stem/example/signing_key_rotation/justfile create mode 100644 packages/stem/example/task_context_mixed/Taskfile.yml delete mode 100644 packages/stem/example/task_context_mixed/justfile create mode 100644 packages/stem/example/unique_tasks/Taskfile.yml delete mode 100644 packages/stem/example/unique_tasks/justfile create mode 100644 packages/stem/example/worker_control_lab/Taskfile.yml delete mode 100644 packages/stem/example/worker_control_lab/justfile create mode 100644 packages/stem/example/workflows/Taskfile.yml delete mode 100644 packages/stem/example/workflows/justfile diff --git a/README.md b/README.md index 046c938e..96c60a4a 100644 --- a/README.md +++ b/README.md @@ -29,48 +29,35 @@ ## Quick Start ```dart +import 'dart:async'; + import 'package:stem/stem.dart'; -// 1. Define a task class EmailTask extends TaskHandler { @override String get name => 'email.send'; - @override - TaskOptions get options => const TaskOptions(maxRetries: 3); - @override Future call(TaskContext ctx, Map args) async { final to = args['to'] as String; - // ... send email logic ... return 'sent to $to'; } } -// 2. Bootstrap and run Future main() async { - final app = await StemApp.inMemory( - tasks: [EmailTask()], - workerConfig: const StemWorkerConfig( - queue: 'default', - consumerName: 'my-worker', - concurrency: 4, - ), - ); - - await app.start(); + final client = await StemClient.inMemory(tasks: [EmailTask()]); + final worker = await client.createWorker(); + unawaited(worker.start()); - // 3. Enqueue work - final taskId = await app.stem.enqueue( + final taskId = await client.stem.enqueue( 'email.send', args: {'to': 'hello@example.com'}, ); + final result = await client.stem.waitForTask(taskId); + print('Result: ${result?.value}'); - // 4. Wait for result - final result = await app.stem.waitForTask(taskId); - print('Result: ${result?.value}'); // "sent to hello@example.com" - - await app.close(); + await worker.shutdown(); + await client.close(); } ``` @@ -154,6 +141,7 @@ Future main() async { | [`stem_sqlite`](./packages/stem_sqlite) | SQLite broker and result backend for local dev/testing | [![pub](https://img.shields.io/pub/v/stem_sqlite.svg)](https://pub.dev/packages/stem_sqlite) | | [`stem_builder`](./packages/stem_builder) | Build-time code generator for annotated tasks and workflows | [![pub](https://img.shields.io/pub/v/stem_builder.svg)](https://pub.dev/packages/stem_builder) | | [`stem_adapter_tests`](./packages/stem_adapter_tests) | Shared contract test suites for adapter implementations | [![pub](https://img.shields.io/pub/v/stem_adapter_tests.svg)](https://pub.dev/packages/stem_adapter_tests) | +| [`stem_memory`](./packages/stem_memory) | In-memory adapter package (broker/backend/workflow/scheduler factories) | [![pub](https://img.shields.io/pub/v/stem_memory.svg)](https://pub.dev/packages/stem_memory) | | [`stem_dashboard`](./packages/dashboard) | Hotwire-based operations dashboard (experimental) | — | --- @@ -284,8 +272,23 @@ task test # Run coverage workflow for core adapters/runtime packages task coverage + +# Run targeted adapter suites (auto-bootstraps integration env) +task test:contract +task test:redis +task test:postgres +``` + +Targeted adapter tasks now bootstrap integration environment automatically. +If bootstrap still fails (for example Docker unavailable), run: + +```bash +source ./packages/stem_cli/_init_test_env ``` +Capability flags and skip behavior for adapter contract suites are documented in +[`packages/stem_adapter_tests/README.md`](./packages/stem_adapter_tests/README.md). + --- ## Contributing diff --git a/Taskfile.yml b/Taskfile.yml index a53fde0b..82333e7b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -30,16 +30,49 @@ includes: dir: ./packages/stem_sqlite tasks: - test: - desc: Run all workspace package tests with integration test env bootstrapped. + test:with-env: + internal: true + requires: + vars: [TASK_NAME, REQUIRED_VARS, RUN] cmds: - | bash -lc ' set -euo pipefail - source ./packages/stem_cli/_init_test_env - task test:no-env + + missing=() + for key in {{.REQUIRED_VARS}}; do + [[ -z "${!key:-}" ]] && missing+=("$key") + done + + if (( ${#missing[@]} > 0 )); then + source ./packages/stem_cli/_init_test_env + missing=() + for key in {{.REQUIRED_VARS}}; do + [[ -z "${!key:-}" ]] && missing+=("$key") + done + fi + + if (( ${#missing[@]} > 0 )); then + echo "DX ERROR: could not bootstrap required integration environment." + echo "Missing vars: ${missing[*]}" + echo "Run manually:" + echo " source ./packages/stem_cli/_init_test_env" + echo "Then rerun: task {{.TASK_NAME}}" + exit 1 + fi + + {{.RUN}} ' + test: + desc: Run all workspace package tests with integration test env bootstrapped. + cmds: + - task: test:with-env + vars: + TASK_NAME: test + REQUIRED_VARS: "STEM_TEST_REDIS_URL STEM_TEST_POSTGRES_URL" + RUN: task test:no-env + test:no-env: desc: Run all workspace package tests without bootstrapping integration env. cmds: @@ -53,15 +86,46 @@ tasks: - task: stem_redis:test - task: stem_sqlite:test + test:contract: + desc: Run adapter contract-heavy suites with shared integration env bootstrap. + cmds: + - task: test:with-env + vars: + TASK_NAME: test:contract + REQUIRED_VARS: "STEM_TEST_REDIS_URL STEM_TEST_POSTGRES_URL" + RUN: | + task stem_adapter_tests:test + task stem_memory:test + task stem_sqlite:test + task stem_redis:test + task stem_postgres:test + + test:redis: + desc: Run Redis package tests with shared integration env bootstrap. + cmds: + - task: test:with-env + vars: + TASK_NAME: test:redis + REQUIRED_VARS: STEM_TEST_REDIS_URL + RUN: task stem_redis:test + + test:postgres: + desc: Run Postgres package tests with shared integration env bootstrap. + cmds: + - task: test:with-env + vars: + TASK_NAME: test:postgres + REQUIRED_VARS: STEM_TEST_POSTGRES_URL + RUN: task stem_postgres:test + coverage: desc: Run coverage workflow with integration test env bootstrapped. cmds: - - | - bash -lc ' - set -euo pipefail - source ./packages/stem_cli/_init_test_env - task coverage:no-env - ' + - task: test:with-env + vars: + TASK_NAME: coverage + REQUIRED_VARS: "STEM_TEST_REDIS_URL STEM_TEST_POSTGRES_URL" + RUN: task coverage:no-env coverage:no-env: desc: Run coverage workflow for core packages (no env bootstrap). diff --git a/packages/dashboard/CHANGELOG.md b/packages/dashboard/CHANGELOG.md new file mode 100644 index 00000000..c45beeb9 --- /dev/null +++ b/packages/dashboard/CHANGELOG.md @@ -0,0 +1,3 @@ +## Unreleased + +- Initial release of the `stem_dashboard` package. diff --git a/packages/stem/CHANGELOG.md b/packages/stem/CHANGELOG.md index a019cdad..df77e2b9 100644 --- a/packages/stem/CHANGELOG.md +++ b/packages/stem/CHANGELOG.md @@ -1,8 +1,17 @@ -## 0.1.1 +## Unreleased +- Improved bootstrap DX with explicit fail-fast errors across broker/backend/ + workflow/schedule/lock/revoke resolution paths in `StemStack.fromUrl`, + including actionable hints when adapters support a URL but do not implement + the requested store kind. +- Refreshed docs to lead with `StemClient` and document adapter-focused Task + workflows. - Aligned in-memory broker and result backend semantics with shared adapter contracts, including broadcast fan-out behavior for in-memory broker tests. - Added Taskfile support for package-scoped test orchestration. +- Added Taskfile-based workflows for complex examples (microservice, encrypted + payloads, signing key rotation, security profiles, and Postgres TLS), + including secret/certificate bootstrap and binary build/run helpers. - Added shared logger injection via `setStemLogger` and reusable structured context helpers for consistent logging metadata across core components. @@ -26,7 +35,7 @@ - Added typed workflow, task, and canvas result APIs with customizable encoders (TaskResultEncoder and payload encoders). - Added new example suites (progress heartbeat, worker control lab, and the - feature-complete set) plus refreshed docs/Justfiles for running them. + feature-complete set) plus refreshed docs/Taskfiles for running them. - Added signals registry/configuration for worker, task, scheduler, and workflow lifecycle events. - Improved worker runtime (isolate pool, config, heartbeats/autoscaling) plus diff --git a/packages/stem/README.md b/packages/stem/README.md index d5e76862..d05f9237 100644 --- a/packages/stem/README.md +++ b/packages/stem/README.md @@ -44,7 +44,6 @@ workflow apps. ```dart import 'dart:async'; import 'package:stem/stem.dart'; -import 'package:stem_redis/stem_redis.dart'; class HelloTask implements TaskHandler { @override @@ -60,11 +59,7 @@ class HelloTask implements TaskHandler { } Future main() async { - final client = await StemClient.create( - broker: StemBrokerFactory.redis(url: 'redis://localhost:6379'), - backend: StemBackendFactory.redis(url: 'redis://localhost:6379/1'), - tasks: [HelloTask()], - ); + final client = await StemClient.inMemory(tasks: [HelloTask()]); final worker = await client.createWorker(); unawaited(worker.start()); @@ -77,6 +72,36 @@ Future main() async { } ``` +For persistent adapters, keep `StemClient` as the entrypoint and resolve +broker/backend wiring from a URL: + +```dart +import 'package:stem/stem.dart'; +import 'package:stem_redis/stem_redis.dart'; + +final client = await StemClient.create( + broker: redisBrokerFactory('redis://localhost:6379'), + backend: redisResultBackendFactory('redis://localhost:6379/1'), + tasks: [HelloTask()], +); +``` + +or use the lower-boilerplate URL helper: + +```dart +import 'package:stem/stem.dart'; +import 'package:stem_redis/stem_redis.dart'; + +final client = await StemClient.fromUrl( + 'redis://localhost:6379', + adapters: const [StemRedisAdapter()], + overrides: const StemStoreOverrides( + backend: 'redis://localhost:6379/1', + ), + tasks: [HelloTask()], +); +``` + ### Direct enqueue (map-based) ```dart @@ -382,9 +407,10 @@ print('Body results: ${chordResult.values}'); ### Task payload encoders By default Stem stores handler arguments/results exactly as provided (JSON-friendly -structures). Configure default `TaskPayloadEncoder`s when bootstrapping `StemApp`, -`StemWorkflowApp`, or `Canvas` to plug in custom serialization (encryption, -compression, base64 wrappers, etc.) for both task arguments and persisted results: +structures). Configure default `TaskPayloadEncoder`s when bootstrapping +`StemClient`, `StemApp`, `StemWorkflowApp`, or `Canvas` to plug in custom +serialization (encryption, compression, base64 wrappers, etc.) for both task +arguments and persisted results: ```dart import 'dart:convert'; @@ -409,7 +435,7 @@ class Base64ResultEncoder extends TaskPayloadEncoder { } } -final app = await StemApp.inMemory( +final client = await StemClient.inMemory( tasks: [...], resultEncoder: const Base64ResultEncoder(), argsEncoder: const Base64ResultEncoder(), diff --git a/packages/stem/Taskfile.yml b/packages/stem/Taskfile.yml index a1908c44..805751ff 100644 --- a/packages/stem/Taskfile.yml +++ b/packages/stem/Taskfile.yml @@ -3,6 +3,11 @@ version: "3" vars: COVERAGE_BADGE: ../../tool/coverage/coverage_badge.dart +includes: + examples: + taskfile: ./example/Taskfile.yml + dir: ./example + tasks: test: desc: Run stem package tests. diff --git a/packages/stem/example/Taskfile.yml b/packages/stem/example/Taskfile.yml new file mode 100644 index 00000000..81e1db8d --- /dev/null +++ b/packages/stem/example/Taskfile.yml @@ -0,0 +1,188 @@ +version: "3" + +includes: + autoscaling_demo: + taskfile: ./autoscaling_demo/Taskfile.yml + dir: ./autoscaling_demo + canvas_patterns: + taskfile: ./canvas_patterns/Taskfile.yml + dir: ./canvas_patterns + daemonized_worker: + taskfile: ./daemonized_worker/Taskfile.yml + dir: ./daemonized_worker + dlq_sandbox: + taskfile: ./dlq_sandbox/Taskfile.yml + dir: ./dlq_sandbox + encrypted_payload: + taskfile: ./encrypted_payload/Taskfile.yml + dir: ./encrypted_payload + microservice: + taskfile: ./microservice/Taskfile.yml + dir: ./microservice + monolith_service: + taskfile: ./monolith_service/Taskfile.yml + dir: ./monolith_service + ops_health_suite: + taskfile: ./ops_health_suite/Taskfile.yml + dir: ./ops_health_suite + postgres_tls: + taskfile: ./postgres_tls/Taskfile.yml + dir: ./postgres_tls + progress_heartbeat: + taskfile: ./progress_heartbeat/Taskfile.yml + dir: ./progress_heartbeat + rate_limit_delay: + taskfile: ./rate_limit_delay/Taskfile.yml + dir: ./rate_limit_delay + retry_task: + taskfile: ./retry_task/Taskfile.yml + dir: ./retry_task + security_ed25519_tls: + taskfile: ./security/ed25519_tls/Taskfile.yml + dir: ./security/ed25519_tls + security_hmac: + taskfile: ./security/hmac/Taskfile.yml + dir: ./security/hmac + security_hmac_tls: + taskfile: ./security/hmac_tls/Taskfile.yml + dir: ./security/hmac_tls + signals_demo: + taskfile: ./signals_demo/Taskfile.yml + dir: ./signals_demo + signing_key_rotation: + taskfile: ./signing_key_rotation/Taskfile.yml + dir: ./signing_key_rotation + task_context_mixed: + taskfile: ./task_context_mixed/Taskfile.yml + dir: ./task_context_mixed + unique_tasks: + taskfile: ./unique_tasks/Taskfile.yml + dir: ./unique_tasks + worker_control_lab: + taskfile: ./worker_control_lab/Taskfile.yml + dir: ./worker_control_lab + workflows: + taskfile: ./workflows/Taskfile.yml + dir: ./workflows + +tasks: + all:build: + desc: Compile binaries for all migrated examples. + cmds: + - task: autoscaling_demo:build + - task: canvas_patterns:build + - task: daemonized_worker:build + - task: dlq_sandbox:build + - task: encrypted_payload:build + - task: microservice:build + - task: monolith_service:build + - task: ops_health_suite:build + - task: postgres_tls:build + - task: progress_heartbeat:build + - task: rate_limit_delay:build + - task: retry_task:build + - task: security_ed25519_tls:build + - task: security_hmac:build + - task: security_hmac_tls:build + - task: signals_demo:build + - task: signing_key_rotation:build + - task: task_context_mixed:build + - task: unique_tasks:build + - task: worker_control_lab:build + - task: workflows:build + + all:deps-up: + desc: Start dependency stacks for all migrated examples that define them. + cmds: + - task: autoscaling_demo:deps-up + - task: dlq_sandbox:deps-up + - task: encrypted_payload:deps-up + - task: microservice:deps-up + - task: ops_health_suite:deps-up + - task: postgres_tls:deps-up + - task: progress_heartbeat:deps-up + - task: rate_limit_delay:deps-up + - task: retry_task:deps-up + - task: security_ed25519_tls:deps-up + - task: security_hmac:deps-up + - task: security_hmac_tls:deps-up + - task: signals_demo:deps-up + - task: signing_key_rotation:deps-up + - task: worker_control_lab:deps-up + + all:deps-down: + desc: Stop dependency stacks for all migrated examples that define them. + cmds: + - task: autoscaling_demo:deps-down + - task: dlq_sandbox:deps-down + - task: encrypted_payload:deps-down + - task: microservice:deps-down + - task: ops_health_suite:deps-down + - task: postgres_tls:deps-down + - task: progress_heartbeat:deps-down + - task: rate_limit_delay:deps-down + - task: retry_task:deps-down + - task: security_ed25519_tls:deps-down + - task: security_hmac:deps-down + - task: security_hmac_tls:deps-down + - task: signals_demo:deps-down + - task: signing_key_rotation:deps-down + - task: worker_control_lab:deps-down + + all:clean: + desc: Remove build artifacts for all migrated examples. + cmds: + - task: autoscaling_demo:clean + - task: canvas_patterns:clean + - task: daemonized_worker:clean + - task: dlq_sandbox:clean + - task: encrypted_payload:clean + - task: microservice:clean + - task: monolith_service:clean + - task: ops_health_suite:clean + - task: postgres_tls:clean + - task: progress_heartbeat:clean + - task: rate_limit_delay:clean + - task: retry_task:clean + - task: security_ed25519_tls:clean + - task: security_hmac:clean + - task: security_hmac_tls:clean + - task: signals_demo:clean + - task: signing_key_rotation:clean + - task: task_context_mixed:clean + - task: unique_tasks:clean + - task: worker_control_lab:clean + - task: workflows:clean + + complex:build: + desc: Compile binaries for all complex/containerized examples. + cmds: + - task: encrypted_payload:build + - task: signing_key_rotation:build + - task: microservice:build + - task: postgres_tls:build + + complex:deps-up: + desc: Start dependency stacks for complex examples. + cmds: + - task: encrypted_payload:deps-up + - task: signing_key_rotation:deps-up + - task: microservice:deps-up + - task: postgres_tls:deps-up + + complex:deps-down: + desc: Stop dependency stacks for complex examples. + cmds: + - task: encrypted_payload:deps-down + - task: signing_key_rotation:deps-down + - task: microservice:deps-down + - task: postgres_tls:deps-down + + security:init: + desc: Bootstrap local security profiles (TLS certs + key rotation). + cmds: + - task: security_hmac:keys:rotate + - task: security_hmac_tls:tls:certs + - task: security_hmac_tls:keys:rotate + - task: security_ed25519_tls:tls:certs + - task: security_ed25519_tls:keys:ed25519 diff --git a/packages/stem/example/autoscaling_demo/README.md b/packages/stem/example/autoscaling_demo/README.md index 94699bc5..b204facd 100644 --- a/packages/stem/example/autoscaling_demo/README.md +++ b/packages/stem/example/autoscaling_demo/README.md @@ -17,15 +17,15 @@ cd example/autoscaling_demo # or from repo root: # cd packages/stem/example/autoscaling_demo -just deps-up -just build +task deps-up +task build # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or use tmux: -just tmux +task tmux ``` You should see log lines like: diff --git a/packages/stem/example/autoscaling_demo/Taskfile.yml b/packages/stem/example/autoscaling_demo/Taskfile.yml new file mode 100644 index 00000000..b1e349a3 --- /dev/null +++ b/packages/stem/example/autoscaling_demo/Taskfile.yml @@ -0,0 +1,81 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}" + dart pub get + dart build cli -t bin/worker.dart -o build/worker + ' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}" + dart pub get + dart build cli -t bin/producer.dart -o build/producer + ' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export STEM_METRIC_EXPORTERS="${STEM_METRIC_EXPORTERS:-console}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run producer flow. + cmds: + - task: run-producer + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/autoscaling_demo/justfile b/packages/stem/example/autoscaling_demo/justfile deleted file mode 100644 index 70ef24a3..00000000 --- a/packages/stem/example/autoscaling_demo/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-autoscaling-demo") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export STEM_METRIC_EXPORTERS="${STEM_METRIC_EXPORTERS:-console}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-producer - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/canvas_patterns/Taskfile.yml b/packages/stem/example/canvas_patterns/Taskfile.yml new file mode 100644 index 00000000..30e92754 --- /dev/null +++ b/packages/stem/example/canvas_patterns/Taskfile.yml @@ -0,0 +1,59 @@ +version: "3" + +tasks: + build: + desc: No build required for canvas pattern demos. + cmds: + - echo "No build step required." + + run-chain: + desc: Run chain canvas demo. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + cd "$stem_root" + dart run example/canvas_patterns/chain_example.dart + ' + + run-chord: + desc: Run chord canvas demo. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + cd "$stem_root" + dart run example/canvas_patterns/chord_example.dart + ' + + run-group: + desc: Run group canvas demo. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + cd "$stem_root" + dart run example/canvas_patterns/group_example.dart + ' + + run: + desc: Run chain demo. + cmds: + - task: run-chain + + clean: + desc: No artifacts to clean. + cmds: + - echo "No build artifacts to clean." diff --git a/packages/stem/example/canvas_patterns/justfile b/packages/stem/example/canvas_patterns/justfile deleted file mode 100644 index ded0f8c0..00000000 --- a/packages/stem/example/canvas_patterns/justfile +++ /dev/null @@ -1,33 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -STEM_ROOT := "{{ROOT}}/../.." -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "") -COMPOSE_ENV := env_var_or_default("COMPOSE_ENV", "") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-canvas-patterns") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) - -build: - @true - -run-chain: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/canvas_patterns/chain_example.dart - -run-chord: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/canvas_patterns/chord_example.dart - -run-group: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/canvas_patterns/group_example.dart - -run: run-chain - -clean: - @true - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run" - {{JUST}} tmux-attach diff --git a/packages/stem/example/daemonized_worker/Taskfile.yml b/packages/stem/example/daemonized_worker/Taskfile.yml new file mode 100644 index 00000000..e087f21d --- /dev/null +++ b/packages/stem/example/daemonized_worker/Taskfile.yml @@ -0,0 +1,31 @@ +version: "3" + +tasks: + build: + desc: No build required for daemonized worker demo. + cmds: + - echo "No build step required." + + run-worker: + desc: Run daemonized worker demo. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + cd "$stem_root" + dart run example/daemonized_worker/bin/worker.dart + ' + + run: + desc: Run daemonized worker. + cmds: + - task: run-worker + + clean: + desc: No artifacts to clean. + cmds: + - echo "No build artifacts to clean." diff --git a/packages/stem/example/daemonized_worker/justfile b/packages/stem/example/daemonized_worker/justfile deleted file mode 100644 index a1f4a056..00000000 --- a/packages/stem/example/daemonized_worker/justfile +++ /dev/null @@ -1,27 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -STEM_ROOT := "{{ROOT}}/../.." -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "") -COMPOSE_ENV := env_var_or_default("COMPOSE_ENV", "") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-daemonized-worker") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) - -build: - @true - -run-worker: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/daemonized_worker/bin/worker.dart - -run: run-worker - -clean: - @true - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run" - {{JUST}} tmux-attach diff --git a/packages/stem/example/dlq_sandbox/README.md b/packages/stem/example/dlq_sandbox/README.md index cf510c83..71621aae 100644 --- a/packages/stem/example/dlq_sandbox/README.md +++ b/packages/stem/example/dlq_sandbox/README.md @@ -101,14 +101,14 @@ Repeat the CLI command with `dlq replay ... --yes` to requeue entries. - The CLI updates result backend metadata with `replayCount`, providing a simple way for the handler to detect a replay. -### Local build + Docker deps (just) +### Local build + Docker deps (task) ```bash -just deps-up -just build +task deps-up +task build # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or: -just tmux +task tmux ``` diff --git a/packages/stem/example/dlq_sandbox/Taskfile.yml b/packages/stem/example/dlq_sandbox/Taskfile.yml new file mode 100644 index 00000000..29f6d76e --- /dev/null +++ b/packages/stem/example/dlq_sandbox/Taskfile.yml @@ -0,0 +1,70 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run worker flow. + cmds: + - task: run-worker + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/dlq_sandbox/justfile b/packages/stem/example/dlq_sandbox/justfile deleted file mode 100644 index cab50ea7..00000000 --- a/packages/stem/example/dlq_sandbox/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-dlq-sandbox") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - dart pub get - dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - dart pub get - dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-worker - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/docs_snippets/lib/signals.dart b/packages/stem/example/docs_snippets/lib/signals.dart index 66fa7b0c..cf94e9ca 100644 --- a/packages/stem/example/docs_snippets/lib/signals.dart +++ b/packages/stem/example/docs_snippets/lib/signals.dart @@ -97,7 +97,8 @@ Future main() async { registerControlSignals(), ]; - final app = await StemApp.create( + final app = await StemApp.fromUrl( + 'memory://', tasks: [ FunctionTaskHandler( name: 'signals.demo', @@ -107,8 +108,6 @@ Future main() async { }, ), ], - broker: StemBrokerFactory.inMemory(), - backend: StemBackendFactory.inMemory(), middleware: buildSignalMiddlewareForProducer(), workerConfig: StemWorkerConfig( middleware: buildSignalMiddlewareForWorker(), diff --git a/packages/stem/example/docs_snippets/lib/uniqueness.dart b/packages/stem/example/docs_snippets/lib/uniqueness.dart index d0538c79..a0a18bec 100644 --- a/packages/stem/example/docs_snippets/lib/uniqueness.dart +++ b/packages/stem/example/docs_snippets/lib/uniqueness.dart @@ -97,13 +97,12 @@ Future enqueueWithOverride(Stem stem) async { // #endregion uniqueness-override-key Future main() async { - final coordinator = buildInMemoryCoordinator(); // #region uniqueness-stem-worker - final app = await StemApp.create( + final app = await StemApp.fromUrl( + 'memory://', tasks: [SendDigestTask()], - broker: StemBrokerFactory.inMemory(), - backend: StemBackendFactory.inMemory(), - uniqueTaskCoordinator: coordinator, + uniqueTasks: true, + uniqueTaskDefaultTtl: const Duration(minutes: 5), workerConfig: const StemWorkerConfig( queue: 'email', consumerName: 'unique-worker', diff --git a/packages/stem/example/docs_snippets/lib/workflows.dart b/packages/stem/example/docs_snippets/lib/workflows.dart index 0dce3918..7097c412 100644 --- a/packages/stem/example/docs_snippets/lib/workflows.dart +++ b/packages/stem/example/docs_snippets/lib/workflows.dart @@ -8,14 +8,15 @@ import 'package:stem_redis/stem_redis.dart'; // #region workflows-runtime Future bootstrapWorkflowRuntime() async { // #region workflows-app-create - final workflowApp = await StemWorkflowApp.create( + final workflowApp = await StemWorkflowApp.fromUrl( + 'redis://127.0.0.1:56379', + adapters: const [StemRedisAdapter(), StemPostgresAdapter()], + overrides: const StemStoreOverrides( + backend: 'redis://127.0.0.1:56379/1', + workflow: 'postgresql://postgres:postgres@127.0.0.1:65432/stem', + ), flows: [ApprovalsFlow.flow], scripts: [retryScript], - broker: redisBrokerFactory('redis://127.0.0.1:56379'), - backend: redisResultBackendFactory('redis://127.0.0.1:56379/1'), - storeFactory: postgresWorkflowStoreFactory( - 'postgresql://postgres:postgres@127.0.0.1:65432/stem', - ), eventBusFactory: WorkflowEventBusFactory.inMemory(), workerConfig: const StemWorkerConfig(queue: 'workflow'), ); @@ -29,7 +30,7 @@ Future bootstrapWorkflowRuntime() async { // #region workflows-client Future bootstrapWorkflowClient() async { - final client = await StemClient.inMemory(); + final client = await StemClient.fromUrl('memory://'); final app = await client.createWorkflowApp(flows: [ApprovalsFlow.flow]); await app.start(); await app.close(); @@ -127,7 +128,8 @@ final encoders = TaskPayloadEncoderRegistry( ); Future configureWorkflowEncoders() async { - final app = await StemWorkflowApp.create( + final app = await StemWorkflowApp.fromUrl( + 'memory://', flows: [ApprovalsFlow.flow], encoderRegistry: encoders, additionalEncoders: const [GzipPayloadEncoder()], diff --git a/packages/stem/example/encrypted_payload/README.md b/packages/stem/example/encrypted_payload/README.md index fab9d57b..f9a378e3 100644 --- a/packages/stem/example/encrypted_payload/README.md +++ b/packages/stem/example/encrypted_payload/README.md @@ -107,16 +107,17 @@ docker run --network host \ container_mixed_encrypted ``` -### Local build + Docker deps (just) +### Local build + Docker deps (task) -By default the Justfile loads `.env`. To use the sample settings, either copy `.env.example` to `.env` or pass `ENV_FILE=.env.example` and update hostnames to `localhost` for local runs. +Initialize a local `.env` (including a fresh payload secret), start Redis, and compile binaries: ```bash -just deps-up -just build +task init +task deps-up +task build # In separate terminals: -just run-worker -just run-enqueuer +task run:worker +task run:enqueuer # Or: -just tmux +task compose-up ``` diff --git a/packages/stem/example/encrypted_payload/Taskfile.yml b/packages/stem/example/encrypted_payload/Taskfile.yml new file mode 100644 index 00000000..d149bf75 --- /dev/null +++ b/packages/stem/example/encrypted_payload/Taskfile.yml @@ -0,0 +1,170 @@ +version: "3" + +tasks: + init: + desc: Create `.env` from `.env.example` (if needed) and rotate `PAYLOAD_SECRET`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="$root/.env" + if [[ ! -f "$env_file" ]]; then + cp "$root/.env.example" "$env_file" + echo "Created $env_file from .env.example" + fi + secret="$(openssl rand -base64 32)" + escaped_secret="$(printf "%s" "$secret" | sed -e "s/[&|]/\\\\&/g")" + if grep -q "^PAYLOAD_SECRET=" "$env_file"; then + sed -i -E "s|^PAYLOAD_SECRET=.*|PAYLOAD_SECRET=${escaped_secret}|" "$env_file" + else + echo "PAYLOAD_SECRET=${secret}" >> "$env_file" + fi + echo "Rotated PAYLOAD_SECRET in $env_file" + ' + + deps-up: + desc: Start Redis dependency only. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start worker + enqueuer containers with Redis. + cmds: + - task: init + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up --build worker enqueuer + + compose-down: + desc: Stop worker + enqueuer containers. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-logs: + desc: Tail container logs. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" logs -f --tail=200 worker enqueuer redis + + build:worker: + desc: Compile encrypted worker binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/worker" + dart pub get + dart build cli -o build + ' + + build:enqueuer: + desc: Compile encrypted enqueuer binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/enqueuer" + dart pub get + dart build cli -o build + ' + + build:container-client: + desc: Compile the standalone containerized encrypted client binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/../.." + dart pub get + dart compile exe example/encrypted_payload/docker/main.dart -o example/encrypted_payload/build/container_mixed_encrypted + ' + + build: + desc: Compile worker, enqueuer, and container client binaries. + deps: [build:worker, build:enqueuer, build:container-client] + + run:worker: + desc: Run compiled encrypted worker locally. + deps: [build:worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}" + export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}" + "$root/worker/build/bundle/bin/"* + ' + + run:enqueuer: + desc: Run compiled encrypted enqueuer locally. + deps: [build:enqueuer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}" + export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}" + "$root/enqueuer/build/bundle/bin/"* + ' + + run:container-client: + desc: Run compiled standalone encrypted container client locally. + deps: [build:container-client] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}" + export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}" + "$root/build/container_mixed_encrypted" + ' + + clean: + desc: Remove compiled artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/worker/build" "{{.TASKFILE_DIR}}/enqueuer/build" "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/encrypted_payload/justfile b/packages/stem/example/encrypted_payload/justfile deleted file mode 100644 index 971eabb6..00000000 --- a/packages/stem/example/encrypted_payload/justfile +++ /dev/null @@ -1,41 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-encrypted-payload") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-enqueuer: - cd "{{ROOT}}/enqueuer"; dart pub get; dart build cli -o "{{BUILD_DIR}}" - -build-worker: - cd "{{ROOT}}/worker"; dart pub get; dart build cli -o "{{BUILD_DIR}}" - -build: build-enqueuer build-worker - -run-enqueuer: - cd "{{ROOT}}/enqueuer"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}"; export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}"; "{{BUILD_DIR}}/bundle/bin"/* - -run-worker: - cd "{{ROOT}}/worker"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}"; export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}"; "{{BUILD_DIR}}/bundle/bin"/* - -run: run-enqueuer - -clean: - rm -rf "{{ROOT}}/enqueuer/{{BUILD_DIR}}" "{{ROOT}}/worker/{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-enqueuer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/microservice/README.md b/packages/stem/example/microservice/README.md index 3a51abe4..d78fc6a0 100644 --- a/packages/stem/example/microservice/README.md +++ b/packages/stem/example/microservice/README.md @@ -38,17 +38,17 @@ Generate a fresh signing secret before production use: ```bash openssl rand -base64 32 -# or ./scripts/security/generate_tls_assets.sh to create TLS assets as well (optional) +# or run `task tls:certs` to create TLS assets as well (optional) ``` Replace the placeholder secret in `.env` with the generated value and update `STEM_SIGNING_ACTIVE_KEY` when rotating keys. To migrate to Ed25519 signing (public/private), run: ```bash -dart run ../../scripts/security/generate_ed25519_keys.dart +task keys:ed25519 ``` -and copy the printed environment variables into your `.env` file. +The task updates `.env` in place. ### Optional: Enabling TLS for Redis @@ -150,27 +150,28 @@ dart pub get dart run bin/beat.dart ``` -## Local build + Docker deps (just) +## Local build + Docker deps (task) -Pick an environment profile and set `ENV_FILE` (or copy one to `.env`): +Pick an environment profile and initialize `.env`: ```bash -cp .env.hmac .env -# or: ENV_FILE=.env.hmac just tmux +task init:hmac +# or: +# task init:hmac-tls +# task init:ed25519-tls ``` -Start dependencies, build binaries, and run in tmux: +Start dependencies, build binaries, and run services locally: ```bash -just deps-up -just build -just tmux +task deps-up +task build ``` If you prefer separate terminals: ```bash -just run-worker -just run-enqueuer -just run-beat +task run:worker +task run:enqueuer +task run:beat ``` diff --git a/packages/stem/example/microservice/Taskfile.yml b/packages/stem/example/microservice/Taskfile.yml new file mode 100644 index 00000000..96d5a795 --- /dev/null +++ b/packages/stem/example/microservice/Taskfile.yml @@ -0,0 +1,266 @@ +version: "3" + +tasks: + init:hmac: + desc: Initialize `.env` from the HMAC profile and rotate the signing secret. + cmds: + - cp "{{.TASKFILE_DIR}}/.env.hmac" "{{.TASKFILE_DIR}}/.env" + - task: secrets:hmac + + init:hmac-tls: + desc: Initialize `.env` from the HMAC+TLS profile, generate certs, and rotate the signing secret. + cmds: + - cp "{{.TASKFILE_DIR}}/.env.hmac_tls" "{{.TASKFILE_DIR}}/.env" + - task: tls:certs + - task: secrets:hmac + + init:ed25519-tls: + desc: Initialize `.env` from the Ed25519+TLS profile, generate certs, and refresh keys. + cmds: + - cp "{{.TASKFILE_DIR}}/.env.ed25519_tls" "{{.TASKFILE_DIR}}/.env" + - task: tls:certs + - task: keys:ed25519 + + secrets:hmac: + desc: Rotate `STEM_SIGNING_KEYS` in `.env` using a fresh random key. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="$root/.env" + if [[ ! -f "$env_file" ]]; then + echo ".env not found. Run: task init:hmac (or init:hmac-tls/init:ed25519-tls)" + exit 1 + fi + secret="$(openssl rand -base64 32)" + escaped_secret="$(printf "%s" "$secret" | sed -e "s/[&|]/\\\\&/g")" + if grep -q "^STEM_SIGNING_KEYS=" "$env_file"; then + sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" + else + echo "STEM_SIGNING_KEYS=primary:${secret}" >> "$env_file" + fi + if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$env_file"; then + sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" + else + echo "STEM_SIGNING_ACTIVE_KEY=primary" >> "$env_file" + fi + echo "Rotated STEM_SIGNING_KEYS in $env_file" + ' + + keys:ed25519: + desc: Generate and apply fresh Ed25519 signing keys into `.env`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + env_file="$root/.env" + if [[ ! -f "$env_file" ]]; then + echo ".env not found. Run: task init:ed25519-tls first." + exit 1 + fi + key_lines="$(cd "$stem_root" && dart run scripts/security/generate_ed25519_keys.dart)" + while IFS= read -r line; do + [[ -z "$line" ]] && continue + key="${line%%=*}" + value="${line#*=}" + escaped_value="$(printf "%s" "$value" | sed -e "s/[&|]/\\\\&/g")" + if grep -q "^${key}=" "$env_file"; then + sed -i -E "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" + else + echo "$key=$value" >> "$env_file" + fi + done <<<"$key_lines" + echo "Updated Ed25519 key material in $env_file" + ' + + tls:certs: + desc: Generate local TLS certs for Redis profiles into `certs/`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + "$stem_root/scripts/security/generate_tls_assets.sh" "$root/certs" "redis" "redis,localhost,127.0.0.1" + ' + + deps-up: + desc: Start dependency services (Redis + observability stack). + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis otel-collector jaeger prometheus grafana + + deps-down: + desc: Stop dependency services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start the full containerized microservice stack. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up --build + + compose-down: + desc: Stop the full containerized microservice stack. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-logs: + desc: Tail logs for core services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" logs -f --tail=200 worker enqueuer beat dashboard redis + + build:worker: + desc: Compile the worker binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/worker" + dart pub get + dart build cli -o build + ' + + build:enqueuer: + desc: Compile the enqueuer API binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/enqueuer" + dart pub get + dart build cli -o build + ' + + build:beat: + desc: Compile the beat binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/beat" + dart pub get + dart build cli -o build + ' + + build:dashboard: + desc: Compile the dashboard binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}/../../../dashboard" + dart pub get + dart build cli -t bin/dashboard.dart -o build + ' + + build: + desc: Compile worker, enqueuer, and beat binaries. + deps: [build:worker, build:enqueuer, build:beat] + + run:worker: + desc: Run the compiled worker binary. + deps: [build:worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/worker/build/bundle/bin/"* + ' + + run:enqueuer: + desc: Run the compiled enqueuer binary. + deps: [build:enqueuer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/enqueuer/build/bundle/bin/"* + ' + + run:beat: + desc: Run the compiled beat binary. + deps: [build:beat] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_SCHEDULE_STORE_URL="${STEM_SCHEDULE_STORE_URL:-redis://localhost:${REDIS_PORT}/2}" + export STEM_SCHEDULE_FILE="${STEM_SCHEDULE_FILE:-$root/schedules.example.yaml}" + "$root/beat/build/bundle/bin/"* + ' + + run:dashboard: + desc: Run the compiled dashboard binary. + deps: [build:dashboard] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + dashboard_root="$root/../../../dashboard" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export DASHBOARD_HOST="${DASHBOARD_HOST:-127.0.0.1}" + export DASHBOARD_PORT="${DASHBOARD_PORT:-3080}" + "$dashboard_root/build/bundle/bin/"* + ' + + clean: + desc: Remove compiled build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/worker/build" "{{.TASKFILE_DIR}}/enqueuer/build" "{{.TASKFILE_DIR}}/beat/build" "{{.TASKFILE_DIR}}/../../../dashboard/build" diff --git a/packages/stem/example/microservice/justfile b/packages/stem/example/microservice/justfile deleted file mode 100644 index 007f3006..00000000 --- a/packages/stem/example/microservice/justfile +++ /dev/null @@ -1,60 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis otel-collector jaeger prometheus grafana") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-microservice") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT + " " + "OTEL_GRPC_PORT=" + OTEL_GRPC_PORT + " " + "OTEL_HTTP_PORT=" + OTEL_HTTP_PORT + " " + "OTEL_PROM_PORT=" + OTEL_PROM_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") -OTEL_GRPC_PORT := env_var_or_default("OTEL_GRPC_PORT", "4317") -OTEL_HTTP_PORT := env_var_or_default("OTEL_HTTP_PORT", "4318") -OTEL_PROM_PORT := env_var_or_default("OTEL_PROM_PORT", "8889") -DASHBOARD_HOST := env_var_or_default("DASHBOARD_HOST", "127.0.0.1") -DASHBOARD_PORT := env_var_or_default("DASHBOARD_PORT", "3080") - - - -build-worker: - cd "{{ROOT}}/worker"; dart pub get; dart build cli -o "{{BUILD_DIR}}" - -build-enqueuer: - cd "{{ROOT}}/enqueuer"; dart pub get; dart build cli -o "{{BUILD_DIR}}" - -build-beat: - cd "{{ROOT}}/beat"; dart pub get; dart build cli -o "{{BUILD_DIR}}" - -build: build-worker build-enqueuer build-beat - -build-dashboard: - cd "{{ROOT}}/../../../dashboard"; dart pub get; dart build cli -t bin/dashboard.dart -o "{{BUILD_DIR}}" - -run-worker: - cd "{{ROOT}}/worker"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/bundle/bin"/* - -run-enqueuer: - cd "{{ROOT}}/enqueuer"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/bundle/bin"/* - -run-beat: - cd "{{ROOT}}/beat"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_SCHEDULE_STORE_URL="${STEM_SCHEDULE_STORE_URL:-redis://localhost:${REDIS_PORT}/2}"; if [ -z "${STEM_SCHEDULE_FILE:-}" ] || [ ! -f "${STEM_SCHEDULE_FILE:-}" ]; then export STEM_SCHEDULE_FILE="{{ROOT}}/schedules.example.yaml"; fi; "{{BUILD_DIR}}/bundle/bin"/* - -run-dashboard: - cd "{{ROOT}}/../../../dashboard"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export DASHBOARD_HOST="${DASHBOARD_HOST:-127.0.0.1}"; export DASHBOARD_PORT="${DASHBOARD_PORT:-3080}"; if [ -d "{{BUILD_DIR}}/bundle/bin" ]; then "{{BUILD_DIR}}/bundle/bin"/*; else dart run bin/dashboard.dart; fi - -run: run-enqueuer - -clean: - rm -rf "{{ROOT}}/worker/{{BUILD_DIR}}" "{{ROOT}}/enqueuer/{{BUILD_DIR}}" "{{ROOT}}/beat/{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-enqueuer" - tmux split-window -t "{{SESSION}}:app" -h "cd \"{{ROOT}}\" && {{JUST}} run-beat" - tmux select-layout -t "{{SESSION}}:app" tiled - tmux new-window -t "{{SESSION}}" -n dashboard "cd \"{{ROOT}}\" && {{JUST}} run-dashboard" - {{JUST}} tmux-attach diff --git a/packages/stem/example/monolith_service/Taskfile.yml b/packages/stem/example/monolith_service/Taskfile.yml new file mode 100644 index 00000000..0327fbb2 --- /dev/null +++ b/packages/stem/example/monolith_service/Taskfile.yml @@ -0,0 +1,26 @@ +version: "3" + +tasks: + build: + desc: Fetch dependencies for monolith service. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get' + + run: + desc: Run monolith service. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + cd "$root" + dart run bin/service.dart + ' + + clean: + desc: Remove local Dart tool state. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/.dart_tool" diff --git a/packages/stem/example/monolith_service/justfile b/packages/stem/example/monolith_service/justfile deleted file mode 100644 index 4db3b989..00000000 --- a/packages/stem/example/monolith_service/justfile +++ /dev/null @@ -1,24 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "") -COMPOSE_ENV := env_var_or_default("COMPOSE_ENV", "") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-monolith-service") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) - -build: - cd "{{ROOT}}" && dart pub get - -run: - cd "{{ROOT}}" && if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi dart run bin/service.dart - -clean: - rm -rf "{{ROOT}}/.dart_tool" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run" - {{JUST}} tmux-attach diff --git a/packages/stem/example/ops_health_suite/README.md b/packages/stem/example/ops_health_suite/README.md index 2b10f381..e6b924bb 100644 --- a/packages/stem/example/ops_health_suite/README.md +++ b/packages/stem/example/ops_health_suite/README.md @@ -16,32 +16,32 @@ cd example/ops_health_suite # or from repo root: # cd packages/stem/example/ops_health_suite -just deps-up -just build +task deps-up +task build # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or use tmux: -just tmux +task tmux ``` ## CLI Health Checks ```bash -just build-cli +task build-cli # Connectivity checks -just stem health +task stem health # Queue + worker snapshots -just stem observe queues -just stem observe workers +task stem observe queues +task stem observe workers # Worker control plane -just stem worker ping --worker ops-worker -just stem worker stats --worker ops-worker +task stem worker ping --worker ops-worker +task stem worker stats --worker ops-worker ``` The worker heartbeats are persisted in Redis and surfaced in `stem observe diff --git a/packages/stem/example/ops_health_suite/Taskfile.yml b/packages/stem/example/ops_health_suite/Taskfile.yml new file mode 100644 index 00000000..edc625c8 --- /dev/null +++ b/packages/stem/example/ops_health_suite/Taskfile.yml @@ -0,0 +1,70 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run producer flow. + cmds: + - task: run-producer + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/ops_health_suite/justfile b/packages/stem/example/ops_health_suite/justfile deleted file mode 100644 index 5c233bce..00000000 --- a/packages/stem/example/ops_health_suite/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-ops-health") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-producer - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/postgres_tls/README.md b/packages/stem/example/postgres_tls/README.md index 4f8680d3..099b058f 100644 --- a/packages/stem/example/postgres_tls/README.md +++ b/packages/stem/example/postgres_tls/README.md @@ -12,30 +12,32 @@ can be reused across brokers and backends. ## Run the demo -1. Start the test services with TLS enabled: +1. Start Redis + Postgres(TLS): ```bash - docker compose -f ../../stem_cli/docker/testing/docker-compose.yml up postgres redis -d + task deps-up ``` 2. Export the environment expected by the scripts: ```bash - export STEM_BROKER_URL=redis://127.0.0.1:6379 - export STEM_RESULT_BACKEND_URL=postgresql://postgres:postgres@127.0.0.1:5432/stem_test - export STEM_TLS_CA_CERT=../../stem_cli/docker/testing/certs/postgres-root.crt - # Optional: allow verification bypass during CA troubleshooting - # export STEM_TLS_ALLOW_INSECURE=true + export STEM_BROKER_URL=redis://127.0.0.1:${REDIS_PORT:-6379} + export STEM_RESULT_BACKEND_URL=postgresql://postgres:postgres@127.0.0.1:${POSTGRES_PORT:-5432}/stem_test + export STEM_TLS_CA_CERT=../../../stem_cli/docker/testing/certs/postgres-root.crt ``` -3. In one shell, run the worker: +3. Compile binaries: ```bash - dart run example/postgres_tls/bin/worker.dart + task build ``` -4. In another shell, enqueue a few tasks: +4. In one shell, run the worker: ```bash - dart run example/postgres_tls/bin/enqueue.dart + task run:worker ``` -5. Watch the worker log the envelopes. TLS handshake failures will surface the +5. In another shell, enqueue a few tasks: + ```bash + task run:enqueue + ``` +6. Watch the worker log the envelopes. TLS handshake failures will surface the CA path, `allowInsecure` flag, and libpq diagnostics to speed up debugging. Stop the containers when you're done: ```bash -docker compose -f ../../stem_cli/docker/testing/docker-compose.yml down +task deps-down ``` diff --git a/packages/stem/example/postgres_tls/Taskfile.yml b/packages/stem/example/postgres_tls/Taskfile.yml new file mode 100644 index 00000000..4df0fbfe --- /dev/null +++ b/packages/stem/example/postgres_tls/Taskfile.yml @@ -0,0 +1,107 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis + Postgres(TLS) dependencies. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis postgres + + deps-down: + desc: Stop Redis + Postgres(TLS) dependencies. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start dependency stack for the TLS demo. + cmds: + - task: deps-up + + compose-down: + desc: Stop dependency stack for the TLS demo. + cmds: + - task: deps-down + + build:worker: + desc: Compile the Postgres TLS worker binary. + cmds: + - | + bash -lc ' + set -euo pipefail + stem_root="{{.TASKFILE_DIR}}/../.." + cd "$stem_root" + dart pub get + dart build cli -t example/postgres_tls/bin/worker.dart -o example/postgres_tls/build/worker + ' + + build:enqueue: + desc: Compile the Postgres TLS enqueue binary. + cmds: + - | + bash -lc ' + set -euo pipefail + stem_root="{{.TASKFILE_DIR}}/../.." + cd "$stem_root" + dart pub get + dart build cli -t example/postgres_tls/bin/enqueue.dart -o example/postgres_tls/build/enqueue + ' + + build: + desc: Compile worker and enqueue binaries. + deps: [build:worker, build:enqueue] + + run:worker: + desc: Run the compiled worker binary. + deps: [build:worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export POSTGRES_PORT="${POSTGRES_PORT:-5432}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://127.0.0.1:${REDIS_PORT}}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-postgresql://postgres:postgres@127.0.0.1:${POSTGRES_PORT}/stem_test}" + export STEM_TLS_CA_CERT="${STEM_TLS_CA_CERT:-$root/../../../stem_cli/docker/testing/certs/postgres-root.crt}" + "$root/build/worker/bundle/bin/"* + ' + + run:enqueue: + desc: Run the compiled enqueue binary. + deps: [build:enqueue] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export POSTGRES_PORT="${POSTGRES_PORT:-5432}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://127.0.0.1:${REDIS_PORT}}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-postgresql://postgres:postgres@127.0.0.1:${POSTGRES_PORT}/stem_test}" + export STEM_TLS_CA_CERT="${STEM_TLS_CA_CERT:-$root/../../../stem_cli/docker/testing/certs/postgres-root.crt}" + "$root/build/enqueue/bundle/bin/"* + ' + + clean: + desc: Remove compiled artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/postgres_tls/docker-compose.yml b/packages/stem/example/postgres_tls/docker-compose.yml new file mode 100644 index 00000000..31cd5c6a --- /dev/null +++ b/packages/stem/example/postgres_tls/docker-compose.yml @@ -0,0 +1,45 @@ +services: + postgres: + build: + context: ../../../stem_cli + dockerfile: docker/testing/postgres/Dockerfile + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: stem_test + command: + - postgres + - -c + - ssl=on + - -c + - ssl_cert_file=/etc/postgres/certs/server.crt + - -c + - ssl_key_file=/etc/postgres/certs/server.key + - -c + - ssl_ca_file=/etc/postgres/certs/root.crt + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "${REDIS_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + +volumes: + postgres-data: diff --git a/packages/stem/example/postgres_tls/justfile b/packages/stem/example/postgres_tls/justfile deleted file mode 100644 index 2e217fc5..00000000 --- a/packages/stem/example/postgres_tls/justfile +++ /dev/null @@ -1,35 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -STEM_ROOT := "{{ROOT}}/../.." -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-postgres-tls") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_FILE := env_var_or_default( - "COMPOSE_FILE", - "{{ROOT}}/../../stem_cli/docker/testing/docker-compose.yml", -) -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "postgres redis") -COMPOSE_ENV := env_var_or_default("COMPOSE_ENV", "") - -build: - @true - -run-worker: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/postgres_tls/bin/worker.dart - -run-enqueue: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/postgres_tls/bin/enqueue.dart - -run: run-worker - -clean: - @true - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-enqueue" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/progress_heartbeat/README.md b/packages/stem/example/progress_heartbeat/README.md index 0fa1ec58..41d2a466 100644 --- a/packages/stem/example/progress_heartbeat/README.md +++ b/packages/stem/example/progress_heartbeat/README.md @@ -36,23 +36,23 @@ From the host, point the CLI at Redis and query worker snapshots: export STEM_BROKER_URL=redis://localhost:6379/0 export STEM_RESULT_BACKEND_URL=redis://localhost:6379/1 -just build-cli -just stem observe workers +task build-cli +task stem observe workers ``` The output includes the worker ID, active count, and the last heartbeat time. -## Local build + Docker deps (just) +## Local build + Docker deps (task) ```bash -just deps-up -just build -just build-cli +task deps-up +task build +task build-cli # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or: -just tmux +task tmux ``` ## Notes diff --git a/packages/stem/example/progress_heartbeat/Taskfile.yml b/packages/stem/example/progress_heartbeat/Taskfile.yml new file mode 100644 index 00000000..ac72a171 --- /dev/null +++ b/packages/stem/example/progress_heartbeat/Taskfile.yml @@ -0,0 +1,71 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export WORKER_NAME="${WORKER_NAME:-progress-worker}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run producer flow. + cmds: + - task: run-producer + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/progress_heartbeat/justfile b/packages/stem/example/progress_heartbeat/justfile deleted file mode 100644 index 9b6e2389..00000000 --- a/packages/stem/example/progress_heartbeat/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-progress-heartbeat") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export WORKER_NAME="${WORKER_NAME:-progress-worker}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-producer - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/rate_limit_delay/README.md b/packages/stem/example/rate_limit_delay/README.md index dcff966a..055721ea 100644 --- a/packages/stem/example/rate_limit_delay/README.md +++ b/packages/stem/example/rate_limit_delay/README.md @@ -86,14 +86,14 @@ docker compose exec worker \ docker compose down ``` -### Local build + Docker deps (just) +### Local build + Docker deps (task) ```bash -just deps-up -just build +task deps-up +task build # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or: -just tmux +task tmux ``` diff --git a/packages/stem/example/rate_limit_delay/Taskfile.yml b/packages/stem/example/rate_limit_delay/Taskfile.yml new file mode 100644 index 00000000..a27a1561 --- /dev/null +++ b/packages/stem/example/rate_limit_delay/Taskfile.yml @@ -0,0 +1,71 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export STEM_RATE_LIMIT_URL="${STEM_RATE_LIMIT_URL:-redis://localhost:${REDIS_PORT}/2}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run worker flow. + cmds: + - task: run-worker + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/rate_limit_delay/justfile b/packages/stem/example/rate_limit_delay/justfile deleted file mode 100644 index 0407f49a..00000000 --- a/packages/stem/example/rate_limit_delay/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-rate-limit-delay") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - dart pub get - dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - dart pub get - dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export STEM_RATE_LIMIT_URL="${STEM_RATE_LIMIT_URL:-redis://localhost:${REDIS_PORT}/2}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-worker - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/retry_task/README.md b/packages/stem/example/retry_task/README.md index 0ff47f5b..bfe8520d 100644 --- a/packages/stem/example/retry_task/README.md +++ b/packages/stem/example/retry_task/README.md @@ -48,14 +48,14 @@ You can also override retries per task via `TaskOptions(maxRetries: N)` in fixed delay instead of exponential backoff, implement a custom `RetryStrategy` or set `TaskOptions.notBefore` inside your own retry logic. -### Local build + Docker deps (just) +### Local build + Docker deps (task) ```bash -just deps-up -just build +task deps-up +task build # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or: -just tmux +task tmux ``` diff --git a/packages/stem/example/retry_task/Taskfile.yml b/packages/stem/example/retry_task/Taskfile.yml new file mode 100644 index 00000000..a2cd1cf5 --- /dev/null +++ b/packages/stem/example/retry_task/Taskfile.yml @@ -0,0 +1,68 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run worker flow. + cmds: + - task: run-worker + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/retry_task/justfile b/packages/stem/example/retry_task/justfile deleted file mode 100644 index 604f8cb5..00000000 --- a/packages/stem/example/retry_task/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-retry-task") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - dart pub get - dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - dart pub get - dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-worker - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/security/ed25519_tls/README.md b/packages/stem/example/security/ed25519_tls/README.md index 442c1c37..e09a4d7a 100644 --- a/packages/stem/example/security/ed25519_tls/README.md +++ b/packages/stem/example/security/ed25519_tls/README.md @@ -7,11 +7,10 @@ This profile turns on Ed25519 public/private key signing in addition to TLS-encr ```bash cd examples/security/ed25519_tls # Produce a new Ed25519 key pair -dart run ../../scripts/security/generate_ed25519_keys.dart -# Update .env with the printed values +task keys:ed25519 # Generate TLS certificates if needed -../../scripts/security/generate_tls_assets.sh certs redis +task tls:certs ``` ## Usage @@ -23,12 +22,16 @@ docker compose up --build Workers trust the public key(s) defined in `.env`, while the enqueuer signs with the private key. Rotate keys periodically following the security runbook. -## Local build + Docker deps (just) +## Local build + Docker deps (task) -The Justfile in this directory runs the microservice binaries locally while using this profile's `.env` for configuration. +The Taskfile in this directory runs the microservice binaries locally while using this profile's `.env.local` for configuration. ```bash -just deps-up -just build -just tmux +task tls:certs +task keys:ed25519 +task deps-up +task build +# in separate terminals: +task run:worker +task run:enqueuer ``` diff --git a/packages/stem/example/security/ed25519_tls/Taskfile.yml b/packages/stem/example/security/ed25519_tls/Taskfile.yml new file mode 100644 index 00000000..867cd0dd --- /dev/null +++ b/packages/stem/example/security/ed25519_tls/Taskfile.yml @@ -0,0 +1,86 @@ +version: "3" + +tasks: + tls:certs: + desc: Generate TLS assets for Redis into `certs/`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../../.." + "$stem_root/scripts/security/generate_tls_assets.sh" "$root/certs" "redis" "redis,localhost,127.0.0.1" + ' + + keys:ed25519: + desc: Generate and apply Ed25519 key material to `.env` and `.env.local`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../../.." + key_lines="$(cd "$stem_root" && dart run scripts/security/generate_ed25519_keys.dart)" + for env_file in "$root/.env" "$root/.env.local"; do + [[ -f "$env_file" ]] || continue + while IFS= read -r line; do + [[ -z "$line" ]] && continue + key="${line%%=*}" + value="${line#*=}" + escaped_value="$(printf "%s" "$value" | sed -e "s/[&|]/\\\\&/g")" + if grep -q "^${key}=" "$env_file"; then + sed -i -E "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" + else + echo "${key}=${value}" >> "$env_file" + fi + done <<<"$key_lines" + done + echo "Updated Ed25519 keys for ed25519_tls profile." + ' + + deps-up: + desc: Start TLS Redis dependency for this profile. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services for this profile. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start profile containers (worker + enqueuer + redis). + cmds: + - task: tls:certs + - task: keys:ed25519 + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up --build + + compose-down: + desc: Stop profile containers. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-logs: + desc: Tail profile container logs. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" logs -f --tail=200 worker enqueuer redis + + build: + desc: Compile microservice worker/enqueuer/beat binaries used by local profile runs. + cmds: + - task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" build + + run:worker: + desc: Run worker locally with this profile's `.env.local`. + cmds: + - ENV_FILE="{{.TASKFILE_DIR}}/.env.local" task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" run:worker + + run:enqueuer: + desc: Run enqueuer locally with this profile's `.env.local`. + cmds: + - ENV_FILE="{{.TASKFILE_DIR}}/.env.local" task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" run:enqueuer + + clean: + desc: Remove compiled microservice artifacts. + cmds: + - task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" clean diff --git a/packages/stem/example/security/ed25519_tls/justfile b/packages/stem/example/security/ed25519_tls/justfile deleted file mode 100644 index e3e384a4..00000000 --- a/packages/stem/example/security/ed25519_tls/justfile +++ /dev/null @@ -1,28 +0,0 @@ -import "../../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -ENV_FILE := env_var_or_default("ENV_FILE", ".env.local") -SESSION := env_var_or_default("SESSION", "stem-security-ed25519-tls") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - -build: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" ENV_FILE="{{ROOT}}/{{ENV_FILE}}" build - -run: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" ENV_FILE="{{ROOT}}/{{ENV_FILE}}" run-enqueuer - -clean: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" clean - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} -f \"{{ROOT}}/../../microservice/justfile\" ENV_FILE=\"{{ROOT}}/{{ENV_FILE}}\" run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} -f \"{{ROOT}}/../../microservice/justfile\" ENV_FILE=\"{{ROOT}}/{{ENV_FILE}}\" run-enqueuer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/security/hmac/README.md b/packages/stem/example/security/hmac/README.md index 60b205ad..7c8b0ca1 100644 --- a/packages/stem/example/security/hmac/README.md +++ b/packages/stem/example/security/hmac/README.md @@ -17,12 +17,15 @@ openssl rand -base64 32 All build contexts point back to `examples/microservice`, so changes there are automatically reflected. -## Local build + Docker deps (just) +## Local build + Docker deps (task) -The Justfile in this directory runs the microservice binaries locally while using this profile's `.env` for configuration. +The Taskfile in this directory runs the microservice binaries locally while using this profile's `.env.local` for configuration. ```bash -just deps-up -just build -just tmux +task keys:rotate +task deps-up +task build +# in separate terminals: +task run:worker +task run:enqueuer ``` diff --git a/packages/stem/example/security/hmac/Taskfile.yml b/packages/stem/example/security/hmac/Taskfile.yml new file mode 100644 index 00000000..f99443b2 --- /dev/null +++ b/packages/stem/example/security/hmac/Taskfile.yml @@ -0,0 +1,74 @@ +version: "3" + +tasks: + keys:rotate: + desc: Rotate HMAC signing key in `.env` and `.env.local`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + secret="$(openssl rand -base64 32)" + escaped_secret="$(printf "%s" "$secret" | sed -e "s/[&|]/\\\\&/g")" + for env_file in "$root/.env" "$root/.env.local"; do + if [[ ! -f "$env_file" ]]; then + continue + fi + if grep -q "^STEM_SIGNING_KEYS=" "$env_file"; then + sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" + else + echo "STEM_SIGNING_KEYS=primary:${secret}" >> "$env_file" + fi + if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$env_file"; then + sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" + else + echo "STEM_SIGNING_ACTIVE_KEY=primary" >> "$env_file" + fi + done + echo "Rotated HMAC signing key for hmac profile." + ' + + deps-up: + desc: Start Redis dependency for this profile. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services for this profile. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start profile containers (worker + enqueuer + redis). + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up --build + + compose-down: + desc: Stop profile containers. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-logs: + desc: Tail profile container logs. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" logs -f --tail=200 worker enqueuer redis + + build: + desc: Compile microservice worker/enqueuer/beat binaries used by local profile runs. + cmds: + - task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" build + + run:worker: + desc: Run worker locally with this profile's `.env.local`. + cmds: + - ENV_FILE="{{.TASKFILE_DIR}}/.env.local" task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" run:worker + + run:enqueuer: + desc: Run enqueuer locally with this profile's `.env.local`. + cmds: + - ENV_FILE="{{.TASKFILE_DIR}}/.env.local" task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" run:enqueuer + + clean: + desc: Remove compiled microservice artifacts. + cmds: + - task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" clean diff --git a/packages/stem/example/security/hmac/justfile b/packages/stem/example/security/hmac/justfile deleted file mode 100644 index f03c7796..00000000 --- a/packages/stem/example/security/hmac/justfile +++ /dev/null @@ -1,28 +0,0 @@ -import "../../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -ENV_FILE := env_var_or_default("ENV_FILE", ".env.local") -SESSION := env_var_or_default("SESSION", "stem-security-hmac") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - -build: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" ENV_FILE="{{ROOT}}/{{ENV_FILE}}" build - -run: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" ENV_FILE="{{ROOT}}/{{ENV_FILE}}" run-enqueuer - -clean: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" clean - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} -f \"{{ROOT}}/../../microservice/justfile\" ENV_FILE=\"{{ROOT}}/{{ENV_FILE}}\" run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} -f \"{{ROOT}}/../../microservice/justfile\" ENV_FILE=\"{{ROOT}}/{{ENV_FILE}}\" run-enqueuer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/security/hmac_tls/README.md b/packages/stem/example/security/hmac_tls/README.md index de3be112..13f96fb1 100644 --- a/packages/stem/example/security/hmac_tls/README.md +++ b/packages/stem/example/security/hmac_tls/README.md @@ -8,7 +8,7 @@ Generate self-signed certificates (or provide your own) before starting: ```bash cd examples/security/hmac_tls -../../scripts/security/generate_tls_assets.sh certs redis +task tls:certs ``` ## Usage @@ -20,12 +20,16 @@ docker compose up --build The `.env` file enables TLS (`rediss://`) and mounts the generated cert bundle. Rotate the shared secret with `openssl rand -base64 32` whenever you redeploy. -## Local build + Docker deps (just) +## Local build + Docker deps (task) -The Justfile in this directory runs the microservice binaries locally while using this profile's `.env` for configuration. +The Taskfile in this directory runs the microservice binaries locally while using this profile's `.env.local` for configuration. ```bash -just deps-up -just build -just tmux +task tls:certs +task keys:rotate +task deps-up +task build +# in separate terminals: +task run:worker +task run:enqueuer ``` diff --git a/packages/stem/example/security/hmac_tls/Taskfile.yml b/packages/stem/example/security/hmac_tls/Taskfile.yml new file mode 100644 index 00000000..a4f4fb38 --- /dev/null +++ b/packages/stem/example/security/hmac_tls/Taskfile.yml @@ -0,0 +1,86 @@ +version: "3" + +tasks: + tls:certs: + desc: Generate TLS assets for Redis into `certs/`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../../.." + "$stem_root/scripts/security/generate_tls_assets.sh" "$root/certs" "redis" "redis,localhost,127.0.0.1" + ' + + keys:rotate: + desc: Rotate HMAC signing key in `.env` and `.env.local`. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + secret="$(openssl rand -base64 32)" + escaped_secret="$(printf "%s" "$secret" | sed -e "s/[&|]/\\\\&/g")" + for env_file in "$root/.env" "$root/.env.local"; do + if [[ ! -f "$env_file" ]]; then + continue + fi + if grep -q "^STEM_SIGNING_KEYS=" "$env_file"; then + sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" + else + echo "STEM_SIGNING_KEYS=primary:${secret}" >> "$env_file" + fi + if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$env_file"; then + sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" + else + echo "STEM_SIGNING_ACTIVE_KEY=primary" >> "$env_file" + fi + done + echo "Rotated HMAC signing key for hmac_tls profile." + ' + + deps-up: + desc: Start TLS Redis dependency for this profile. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services for this profile. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start profile containers (worker + enqueuer + redis). + cmds: + - task: tls:certs + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up --build + + compose-down: + desc: Stop profile containers. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-logs: + desc: Tail profile container logs. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" logs -f --tail=200 worker enqueuer redis + + build: + desc: Compile microservice worker/enqueuer/beat binaries used by local profile runs. + cmds: + - task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" build + + run:worker: + desc: Run worker locally with this profile's `.env.local`. + cmds: + - ENV_FILE="{{.TASKFILE_DIR}}/.env.local" task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" run:worker + + run:enqueuer: + desc: Run enqueuer locally with this profile's `.env.local`. + cmds: + - ENV_FILE="{{.TASKFILE_DIR}}/.env.local" task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" run:enqueuer + + clean: + desc: Remove compiled microservice artifacts. + cmds: + - task -t "{{.TASKFILE_DIR}}/../../microservice/Taskfile.yml" clean diff --git a/packages/stem/example/security/hmac_tls/justfile b/packages/stem/example/security/hmac_tls/justfile deleted file mode 100644 index 7f05f098..00000000 --- a/packages/stem/example/security/hmac_tls/justfile +++ /dev/null @@ -1,28 +0,0 @@ -import "../../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -ENV_FILE := env_var_or_default("ENV_FILE", ".env.local") -SESSION := env_var_or_default("SESSION", "stem-security-hmac-tls") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - -build: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" ENV_FILE="{{ROOT}}/{{ENV_FILE}}" build - -run: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" ENV_FILE="{{ROOT}}/{{ENV_FILE}}" run-enqueuer - -clean: - {{JUST}} -f "{{ROOT}}/../../microservice/justfile" clean - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} -f \"{{ROOT}}/../../microservice/justfile\" ENV_FILE=\"{{ROOT}}/{{ENV_FILE}}\" run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} -f \"{{ROOT}}/../../microservice/justfile\" ENV_FILE=\"{{ROOT}}/{{ENV_FILE}}\" run-enqueuer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/signals_demo/README.md b/packages/stem/example/signals_demo/README.md index 6ebde46e..829601bf 100644 --- a/packages/stem/example/signals_demo/README.md +++ b/packages/stem/example/signals_demo/README.md @@ -29,14 +29,14 @@ Stop the demo with `Ctrl+C`. The worker and producer trap TERM signals and shut down gracefully. To rebuild after editing the example, run `docker compose build`. -### Local build + Docker deps (just) +### Local build + Docker deps (task) ```bash -just deps-up -just build +task deps-up +task build # In separate terminals: -just run-worker -just run-producer +task run-worker +task run-producer # Or: -just tmux +task tmux ``` diff --git a/packages/stem/example/signals_demo/Taskfile.yml b/packages/stem/example/signals_demo/Taskfile.yml new file mode 100644 index 00000000..a2cd1cf5 --- /dev/null +++ b/packages/stem/example/signals_demo/Taskfile.yml @@ -0,0 +1,68 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run worker flow. + cmds: + - task: run-worker + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/signals_demo/justfile b/packages/stem/example/signals_demo/justfile deleted file mode 100644 index f058f3ef..00000000 --- a/packages/stem/example/signals_demo/justfile +++ /dev/null @@ -1,43 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-signals-demo") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - dart pub get - dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - dart pub get - dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-worker - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/signing_key_rotation/README.md b/packages/stem/example/signing_key_rotation/README.md index 4b78c6c0..919c4811 100644 --- a/packages/stem/example/signing_key_rotation/README.md +++ b/packages/stem/example/signing_key_rotation/README.md @@ -17,17 +17,18 @@ cd example/signing_key_rotation # or from repo root: # cd packages/stem/example/signing_key_rotation -just deps-up -just build +task deps-up +task build +task keys:rotate # Terminal 1: start the worker (uses .env with both keys) -just run-worker +task run:worker # Terminal 2: enqueue with the primary key -just run-producer-primary +task run:producer-primary # Rotate: enqueue again with the rotated key -just run-producer-rotated +task run:producer-rotated ``` The worker should process tasks from both runs without error, proving the @@ -43,4 +44,4 @@ rotation overlap works. - If you remove a key from `STEM_SIGNING_KEYS`, any tasks signed with that key will be rejected and sent to the DLQ (`reason=signature-invalid`). -- You can verify DLQ contents with `just build-cli` and `just stem dlq list`. +- You can verify DLQ contents with the `stem` CLI (`stem dlq list`). diff --git a/packages/stem/example/signing_key_rotation/Taskfile.yml b/packages/stem/example/signing_key_rotation/Taskfile.yml new file mode 100644 index 00000000..a9f42b0e --- /dev/null +++ b/packages/stem/example/signing_key_rotation/Taskfile.yml @@ -0,0 +1,152 @@ +version: "3" + +tasks: + keys:rotate: + desc: Rotate primary/rotated HMAC keys across all local env profiles. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + files=("$root/.env" "$root/.env.primary" "$root/.env.rotate") + for file in "${files[@]}"; do + if [[ ! -f "$file" ]]; then + echo "Missing $file" + exit 1 + fi + done + + primary="$(openssl rand -base64 32)" + rotated="$(openssl rand -base64 32)" + keys_value="primary:${primary},rotated:${rotated}" + escaped_keys="$(printf "%s" "$keys_value" | sed -e "s/[&|]/\\\\&/g")" + + for file in "${files[@]}"; do + if grep -q "^STEM_SIGNING_KEYS=" "$file"; then + sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=${escaped_keys}|" "$file" + else + echo "STEM_SIGNING_KEYS=${keys_value}" >> "$file" + fi + + active="primary" + if [[ "$file" == *".env.rotate" ]]; then + active="rotated" + fi + if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$file"; then + sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=${active}|" "$file" + else + echo "STEM_SIGNING_ACTIVE_KEY=${active}" >> "$file" + fi + done + + echo "Rotated signing keys in .env, .env.primary, and .env.rotate" + ' + + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + compose-up: + desc: Start Redis for the rotation drill. + cmds: + - task: deps-up + + compose-down: + desc: Stop Redis for the rotation drill. + cmds: + - task: deps-down + + build:worker: + desc: Compile worker binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}" + dart pub get + dart build cli -t bin/worker.dart -o build/worker + ' + + build:producer: + desc: Compile producer binary. + cmds: + - | + bash -lc ' + set -euo pipefail + cd "{{.TASKFILE_DIR}}" + dart pub get + dart build cli -t bin/producer.dart -o build/producer + ' + + build: + desc: Compile worker and producer binaries. + deps: [build:worker, build:producer] + + run:worker: + desc: Run compiled worker using `.env` (verifies both keys). + deps: [build:worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/worker/bundle/bin/"* + ' + + run:producer: + desc: Run compiled producer (ENV_FILE controls active key profile). + deps: [build:producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env.primary}" + if [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + elif [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run:producer-primary: + desc: Run producer with `.env.primary`. + cmds: + - ENV_FILE=.env.primary task -t "{{.TASKFILE_DIR}}/Taskfile.yml" run:producer + + run:producer-rotated: + desc: Run producer with `.env.rotate`. + cmds: + - ENV_FILE=.env.rotate task -t "{{.TASKFILE_DIR}}/Taskfile.yml" run:producer + + clean: + desc: Remove compiled artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/signing_key_rotation/justfile b/packages/stem/example/signing_key_rotation/justfile deleted file mode 100644 index 54e17aac..00000000 --- a/packages/stem/example/signing_key_rotation/justfile +++ /dev/null @@ -1,49 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-signing-rotation") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run-producer-primary: - ENV_FILE=".env.primary" {{JUST}} run-producer - -run-producer-rotated: - ENV_FILE=".env.rotate" {{JUST}} run-producer - -run: run-producer - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-producer-primary" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/stack_autowire.dart b/packages/stem/example/stack_autowire.dart index d23f3074..f1a3a4e2 100644 --- a/packages/stem/example/stack_autowire.dart +++ b/packages/stem/example/stack_autowire.dart @@ -7,6 +7,9 @@ class PingTask implements TaskHandler { @override String get name => 'demo.ping'; + @override + TaskMetadata get metadata => const TaskMetadata(); + @override TaskOptions get options => const TaskOptions(maxRetries: 0); @@ -20,13 +23,12 @@ class PingTask implements TaskHandler { } Future main() async { + const redisAdapters = [StemRedisAdapter()]; final stack = StemStack.fromUrl( 'redis://localhost:6379/0', - adapters: const [StemRedisAdapter()], - workflows: true, + adapters: redisAdapters, scheduling: true, uniqueTasks: true, - requireRevokeStore: true, ); final scheduleFactory = stack.scheduleStore; @@ -37,26 +39,19 @@ Future main() async { if (lockFactory == null) { throw StateError('Unique tasks enabled but lock store factory missing.'); } - final revokeFactory = stack.revokeStore; - if (revokeFactory == null) { - throw StateError('Revoke store required but factory missing.'); - } - final scheduleStore = await scheduleFactory.create(); final lockStore = await lockFactory.create(); - final revokeStore = await revokeFactory.create(); - - final app = await StemApp.create( + final app = await StemApp.fromUrl( + 'redis://localhost:6379/0', tasks: [PingTask()], - broker: stack.broker, - backend: stack.backend, - revokeStore: revokeStore, + adapters: redisAdapters, + uniqueTasks: true, + requireRevokeStore: true, ); - final workflowApp = await StemWorkflowApp.create( - broker: stack.broker, - backend: stack.backend, - storeFactory: stack.workflowStore, + final workflowApp = await StemWorkflowApp.fromUrl( + 'redis://localhost:6379/0', + adapters: redisAdapters, ); final beat = Beat( @@ -78,6 +73,5 @@ Future main() async { await app.shutdown(); await scheduleFactory.dispose(scheduleStore); await lockFactory.dispose(lockStore); - await revokeFactory.dispose(revokeStore); } } diff --git a/packages/stem/example/task_context_mixed/README.md b/packages/stem/example/task_context_mixed/README.md index 5bdb29de..5cd70d88 100644 --- a/packages/stem/example/task_context_mixed/README.md +++ b/packages/stem/example/task_context_mixed/README.md @@ -26,7 +26,7 @@ dart run bin/enqueue.dart ``` For best results with `sqlite3` native assets, build the CLI binaries first -(`dart build cli`). The included `justfile` does this automatically. +(`dart build cli`). The included `Taskfile.yml` does this automatically. Optional flags: @@ -63,13 +63,13 @@ separate SQLite files for the broker and backend to avoid WAL contention and keeps the producer disconnected from the backend (so only the worker writes result state). -### Local build + just +### Local build + task ```bash -just build +task build # In separate terminals: -just run-worker -just run-enqueue +task run-worker +task run-enqueue # Or: -just tmux +task tmux ``` diff --git a/packages/stem/example/task_context_mixed/Taskfile.yml b/packages/stem/example/task_context_mixed/Taskfile.yml new file mode 100644 index 00000000..719f98dd --- /dev/null +++ b/packages/stem/example/task_context_mixed/Taskfile.yml @@ -0,0 +1,101 @@ +version: "3" + +tasks: + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-enqueue: + desc: Compile enqueue binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/enqueue.dart -o build/enqueue' + + build: + desc: Compile worker and enqueue binaries. + deps: [build-worker, build-enqueue] + + run-worker: + desc: Run worker (compiled binary when available). + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" + export STEM_SQLITE_BACKEND_PATH="${STEM_SQLITE_BACKEND_PATH:-task_context_mixed_backend.sqlite}" + if [[ -d "$root/build/worker/bundle/bin" ]]; then + "$root/build/worker/bundle/bin/"* + else + cd "$root" + dart run bin/worker.dart + fi + ' + + run-enqueue: + desc: Run enqueue command (compiled binary when available). + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" + if [[ -d "$root/build/enqueue/bundle/bin" ]]; then + "$root/build/enqueue/bundle/bin/"* + else + cd "$root" + dart run bin/enqueue.dart + fi + ' + + run-fail: + desc: Enqueue failing payload path. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" + if [[ -d "$root/build/enqueue/bundle/bin" ]]; then + "$root/build/enqueue/bundle/bin/"* --fail + else + cd "$root" + dart run bin/enqueue.dart --fail + fi + ' + + run-overwrite: + desc: Enqueue overwrite path. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" + if [[ -d "$root/build/enqueue/bundle/bin" ]]; then + "$root/build/enqueue/bundle/bin/"* --overwrite + else + cd "$root" + dart run bin/enqueue.dart --overwrite + fi + ' + + run: + desc: Run enqueue flow. + cmds: + - task: run-enqueue + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/task_context_mixed/justfile b/packages/stem/example/task_context_mixed/justfile deleted file mode 100644 index 43790f2b..00000000 --- a/packages/stem/example/task_context_mixed/justfile +++ /dev/null @@ -1,53 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-task-context-mixed") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "" -BROKER_PATH := env_var_or_default( - "STEM_SQLITE_BROKER_PATH", - "task_context_mixed_broker.sqlite", -) -BACKEND_PATH := env_var_or_default( - "STEM_SQLITE_BACKEND_PATH", - "task_context_mixed_backend.sqlite", -) - -build-worker: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-enqueue: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/enqueue.dart -o "{{BUILD_DIR}}/enqueue" - -build: build-worker build-enqueue - -run-worker: - {{ENV_LOAD}}; export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-{{BROKER_PATH}}}"; export STEM_SQLITE_BACKEND_PATH="${STEM_SQLITE_BACKEND_PATH:-{{BACKEND_PATH}}}"; if [ -d "{{BUILD_DIR}}/worker/bundle/bin" ]; then "{{BUILD_DIR}}/worker/bundle/bin"/*; else cd "{{ROOT}}"; dart run bin/worker.dart; fi - -run-enqueue: - {{ENV_LOAD}}; export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-{{BROKER_PATH}}}"; if [ -d "{{BUILD_DIR}}/enqueue/bundle/bin" ]; then "{{BUILD_DIR}}/enqueue/bundle/bin"/*; else cd "{{ROOT}}"; dart run bin/enqueue.dart; fi - -run-fail: - {{ENV_LOAD}}; export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-{{BROKER_PATH}}}"; if [ -d "{{BUILD_DIR}}/enqueue/bundle/bin" ]; then "{{BUILD_DIR}}/enqueue/bundle/bin"/* --fail; else cd "{{ROOT}}"; dart run bin/enqueue.dart --fail; fi - -run-overwrite: - {{ENV_LOAD}}; export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-{{BROKER_PATH}}}"; if [ -d "{{BUILD_DIR}}/enqueue/bundle/bin" ]; then "{{BUILD_DIR}}/enqueue/bundle/bin"/* --overwrite; else cd "{{ROOT}}"; dart run bin/enqueue.dart --overwrite; fi - -run: run-enqueue - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && {{JUST}} run-enqueue" - tmux select-layout -t "{{SESSION}}:app" even-vertical - {{JUST}} tmux-attach diff --git a/packages/stem/example/unique_tasks/Taskfile.yml b/packages/stem/example/unique_tasks/Taskfile.yml new file mode 100644 index 00000000..b5485494 --- /dev/null +++ b/packages/stem/example/unique_tasks/Taskfile.yml @@ -0,0 +1,26 @@ +version: "3" + +tasks: + build: + desc: No build required for unique task demo. + cmds: + - echo "No build step required." + + run: + desc: Run unique task demo. + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + stem_root="$root/../.." + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + cd "$stem_root" + dart run example/unique_tasks/unique_task_example.dart + ' + + clean: + desc: No artifacts to clean. + cmds: + - echo "No build artifacts to clean." diff --git a/packages/stem/example/unique_tasks/justfile b/packages/stem/example/unique_tasks/justfile deleted file mode 100644 index 08655c60..00000000 --- a/packages/stem/example/unique_tasks/justfile +++ /dev/null @@ -1,25 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -STEM_ROOT := "{{ROOT}}/../.." -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "") -COMPOSE_ENV := env_var_or_default("COMPOSE_ENV", "") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-unique-tasks") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) - -build: - @true - -run: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/unique_tasks/unique_task_example.dart - -clean: - @true - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run" - {{JUST}} tmux-attach diff --git a/packages/stem/example/worker_control_lab/README.md b/packages/stem/example/worker_control_lab/README.md index f0723b6b..03211778 100644 --- a/packages/stem/example/worker_control_lab/README.md +++ b/packages/stem/example/worker_control_lab/README.md @@ -34,28 +34,28 @@ export STEM_REVOKE_STORE_URL=redis://localhost:6379/2 # Ping both workers -just build-cli -just stem worker ping \ +task build-cli +task stem worker ping \ --worker control-alpha --worker control-bravo # Inspect worker stats -just stem worker stats --worker control-alpha +task stem worker stats --worker control-alpha # Inspect active tasks and revocations -just stem worker inspect --worker control-alpha +task stem worker inspect --worker control-alpha # Revoke a long-running task (terminate it if already running) -just stem worker revoke \ +task stem worker revoke \ --task \ --terminate \ --reason "demo revoke" # Request a warm shutdown for one worker -just stem worker shutdown \ +task stem worker shutdown \ --worker control-bravo \ --mode warm ``` @@ -66,18 +66,18 @@ Stop the stack with: docker compose down ``` -## Local build + Docker deps (just) +## Local build + Docker deps (task) ```bash -just deps-up -just build -just build-cli +task deps-up +task build +task build-cli # In separate terminals: -WORKER_NAME=control-alpha just run-worker -WORKER_NAME=control-bravo just run-worker -just run-producer +WORKER_NAME=control-alpha task run-worker +WORKER_NAME=control-bravo task run-worker +task run-producer # Or: -just tmux +task tmux ``` When running locally, export the same `STEM_*` variables before using the CLI: diff --git a/packages/stem/example/worker_control_lab/Taskfile.yml b/packages/stem/example/worker_control_lab/Taskfile.yml new file mode 100644 index 00000000..0a4e99f3 --- /dev/null +++ b/packages/stem/example/worker_control_lab/Taskfile.yml @@ -0,0 +1,73 @@ +version: "3" + +tasks: + deps-up: + desc: Start Redis dependency. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up -d redis + + deps-down: + desc: Stop docker-compose services. + cmds: + - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" down + + build-worker: + desc: Compile worker binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/worker.dart -o build/worker' + + build-producer: + desc: Compile producer binary. + cmds: + - | + bash -lc 'set -euo pipefail; cd "{{.TASKFILE_DIR}}"; dart pub get; dart build cli -t bin/producer.dart -o build/producer' + + build: + desc: Compile worker and producer binaries. + deps: [build-worker, build-producer] + + run-worker: + desc: Run worker binary. + deps: [build-worker] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + export STEM_REVOKE_STORE_URL="${STEM_REVOKE_STORE_URL:-redis://localhost:${REDIS_PORT}/2}" + export WORKER_NAME="${WORKER_NAME:-control-worker}" + export WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-2}" + "$root/build/worker/bundle/bin/"* + ' + + run-producer: + desc: Run producer binary. + deps: [build-producer] + cmds: + - | + bash -lc ' + set -euo pipefail + root="{{.TASKFILE_DIR}}" + env_file="${ENV_FILE:-.env}" + if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + export REDIS_PORT="${REDIS_PORT:-6379}" + export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" + "$root/build/producer/bundle/bin/"* + ' + + run: + desc: Run producer flow. + cmds: + - task: run-producer + + clean: + desc: Remove build artifacts. + cmds: + - rm -rf "{{.TASKFILE_DIR}}/build" diff --git a/packages/stem/example/worker_control_lab/justfile b/packages/stem/example/worker_control_lab/justfile deleted file mode 100644 index 44f74829..00000000 --- a/packages/stem/example/worker_control_lab/justfile +++ /dev/null @@ -1,44 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() - -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "docker-compose.yml") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "redis") -BUILD_DIR := env_var_or_default("BUILD_DIR", "build") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-worker-control") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) -COMPOSE_ENV := "REDIS_PORT=" + REDIS_PORT -REDIS_PORT := env_var_or_default("REDIS_PORT", "6379") - - - -build-worker: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/worker.dart -o "{{BUILD_DIR}}/worker" - -build-producer: - cd "{{ROOT}}"; dart pub get - cd "{{ROOT}}"; dart build cli -t bin/producer.dart -o "{{BUILD_DIR}}/producer" - -build: build-worker build-producer - -run-worker: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; export STEM_REVOKE_STORE_URL="${STEM_REVOKE_STORE_URL:-redis://localhost:${REDIS_PORT}/2}"; export WORKER_NAME="${WORKER_NAME:-control-worker}"; export WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-2}"; "{{BUILD_DIR}}/worker/bundle/bin"/* - -run-producer: - cd "{{ROOT}}"; if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi; export REDIS_PORT="${REDIS_PORT:-6379}"; export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}"; export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}"; "{{BUILD_DIR}}/producer/bundle/bin"/* - -run: run-producer - -clean: - rm -rf "{{BUILD_DIR}}" - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && WORKER_NAME=control-alpha {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -v "cd \"{{ROOT}}\" && WORKER_NAME=control-bravo {{JUST}} run-worker" - tmux split-window -t "{{SESSION}}:app" -h "cd \"{{ROOT}}\" && {{JUST}} run-producer" - tmux select-layout -t "{{SESSION}}:app" tiled - {{JUST}} tmux-attach diff --git a/packages/stem/example/workflows/Taskfile.yml b/packages/stem/example/workflows/Taskfile.yml new file mode 100644 index 00000000..028bf8b4 --- /dev/null +++ b/packages/stem/example/workflows/Taskfile.yml @@ -0,0 +1,53 @@ +version: "3" + +tasks: + build: + desc: No build required for workflow script demos. + cmds: + - echo "No build step required." + + run-basic: + desc: Run basic in-memory workflow example. + cmds: + - | + bash -lc 'set -euo pipefail; root="{{.TASKFILE_DIR}}"; stem_root="$root/../.."; env_file="${ENV_FILE:-.env}"; if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi; cd "$stem_root"; dart run example/workflows/basic_in_memory.dart' + + run-cancel: + desc: Run cancellation policy workflow example. + cmds: + - | + bash -lc 'set -euo pipefail; root="{{.TASKFILE_DIR}}"; stem_root="$root/../.."; env_file="${ENV_FILE:-.env}"; if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi; cd "$stem_root"; dart run example/workflows/cancellation_policy.dart' + + run-custom: + desc: Run custom factory workflow example. + cmds: + - | + bash -lc 'set -euo pipefail; root="{{.TASKFILE_DIR}}"; stem_root="$root/../.."; env_file="${ENV_FILE:-.env}"; if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi; cd "$stem_root"; dart run example/workflows/custom_factories.dart' + + run-sleep: + desc: Run sleep and event workflow example. + cmds: + - | + bash -lc 'set -euo pipefail; root="{{.TASKFILE_DIR}}"; stem_root="$root/../.."; env_file="${ENV_FILE:-.env}"; if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi; cd "$stem_root"; dart run example/workflows/sleep_and_event.dart' + + run-sqlite: + desc: Run sqlite workflow example. + cmds: + - | + bash -lc 'set -euo pipefail; root="{{.TASKFILE_DIR}}"; stem_root="$root/../.."; env_file="${ENV_FILE:-.env}"; if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi; cd "$stem_root"; dart run example/workflows/sqlite_store.dart' + + run-versioned: + desc: Run versioned rewind workflow example. + cmds: + - | + bash -lc 'set -euo pipefail; root="{{.TASKFILE_DIR}}"; stem_root="$root/../.."; env_file="${ENV_FILE:-.env}"; if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi; cd "$stem_root"; dart run example/workflows/versioned_rewind.dart' + + run: + desc: Run basic workflow demo. + cmds: + - task: run-basic + + clean: + desc: No artifacts to clean. + cmds: + - echo "No build artifacts to clean." diff --git a/packages/stem/example/workflows/custom_factories.dart b/packages/stem/example/workflows/custom_factories.dart index 6ce861fd..ee7a47b8 100644 --- a/packages/stem/example/workflows/custom_factories.dart +++ b/packages/stem/example/workflows/custom_factories.dart @@ -5,7 +5,13 @@ import 'package:stem/stem.dart'; import 'package:stem_redis/stem_redis.dart'; Future main() async { - final app = await StemWorkflowApp.create( + final app = await StemWorkflowApp.fromUrl( + 'redis://localhost:6379', + adapters: const [StemRedisAdapter()], + overrides: const StemStoreOverrides( + backend: 'redis://localhost:6379/1', + workflow: 'redis://localhost:6379/2', + ), flows: [ Flow( name: 'redis.workflow', @@ -14,9 +20,6 @@ Future main() async { }, ), ], - broker: redisBrokerFactory('redis://localhost:6379'), - backend: redisResultBackendFactory('redis://localhost:6379/1'), - storeFactory: redisWorkflowStoreFactory('redis://localhost:6379/2'), ); try { diff --git a/packages/stem/example/workflows/justfile b/packages/stem/example/workflows/justfile deleted file mode 100644 index 541d563c..00000000 --- a/packages/stem/example/workflows/justfile +++ /dev/null @@ -1,42 +0,0 @@ -import "../Justfile.common" - -ROOT := justfile_directory() -STEM_ROOT := "{{ROOT}}/../.." -COMPOSE_FILE := env_var_or_default("COMPOSE_FILE", "") -DEPS_SERVICES := env_var_or_default("DEPS_SERVICES", "") -COMPOSE_ENV := env_var_or_default("COMPOSE_ENV", "") -ENV_FILE := env_var_or_default("ENV_FILE", ".env") -SESSION := env_var_or_default("SESSION", "stem-workflows") -PROJECT_NAME := env_var_or_default("PROJECT_NAME", SESSION) - -build: - @true - -run-basic: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/workflows/basic_in_memory.dart - -run-cancel: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/workflows/cancellation_policy.dart - -run-custom: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/workflows/custom_factories.dart - -run-sleep: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/workflows/sleep_and_event.dart - -run-sqlite: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/workflows/sqlite_store.dart - -run-versioned: - if [ -f "{{ENV_FILE}}" ]; then set -a; . "{{ENV_FILE}}"; set +a; elif [ -f "{{ROOT}}/{{ENV_FILE}}" ]; then set -a; . "{{ROOT}}/{{ENV_FILE}}"; set +a; fi cd "{{STEM_ROOT}}" && dart run example/workflows/versioned_rewind.dart - -run: run-basic - -clean: - @true - -tmux: - {{JUST}} tmux-guard - {{JUST}} tmux-prep - tmux new-session -d -s "{{SESSION}}" -n app "cd \"{{ROOT}}\" && {{JUST}} run" - {{JUST}} tmux-attach diff --git a/packages/stem/example/workflows/sqlite_store.dart b/packages/stem/example/workflows/sqlite_store.dart index a680bc17..f3cda604 100644 --- a/packages/stem/example/workflows/sqlite_store.dart +++ b/packages/stem/example/workflows/sqlite_store.dart @@ -8,7 +8,9 @@ import 'package:stem_sqlite/stem_sqlite.dart'; Future main() async { final databaseFile = File('workflow.sqlite'); - final app = await StemWorkflowApp.create( + final app = await StemWorkflowApp.fromUrl( + 'sqlite://${databaseFile.path}', + adapters: const [StemSqliteAdapter()], flows: [ Flow( name: 'sqlite.example', @@ -17,7 +19,6 @@ Future main() async { }, ), ], - storeFactory: sqliteWorkflowStoreFactory(databaseFile), ); try { diff --git a/packages/stem/lib/src/bootstrap/stem_app.dart b/packages/stem/lib/src/bootstrap/stem_app.dart index 290bbc75..5e15c1d4 100644 --- a/packages/stem/lib/src/bootstrap/stem_app.dart +++ b/packages/stem/lib/src/bootstrap/stem_app.dart @@ -1,6 +1,7 @@ import 'package:stem/src/backend/encoding_result_backend.dart'; import 'package:stem/src/bootstrap/factories.dart'; import 'package:stem/src/bootstrap/stem_client.dart'; +import 'package:stem/src/bootstrap/stem_stack.dart'; import 'package:stem/src/canvas/canvas.dart'; import 'package:stem/src/control/in_memory_revoke_store.dart'; import 'package:stem/src/control/revoke_store.dart'; @@ -202,6 +203,107 @@ class StemApp { ); } + /// Creates an app from a single backend URL plus adapter wiring. + /// + /// This helper resolves broker/backend factories via [StemStack.fromUrl] and + /// can optionally auto-wire revoke and unique-task coordination stores. + static Future fromUrl( + String url, { + Iterable> tasks = const [], + TaskRegistry? registry, + Iterable adapters = const [], + StemStoreOverrides overrides = const StemStoreOverrides(), + StemWorkerConfig workerConfig = const StemWorkerConfig(), + RevokeStore? revokeStore, + UniqueTaskCoordinator? uniqueTaskCoordinator, + bool uniqueTasks = false, + Duration uniqueTaskDefaultTtl = const Duration(minutes: 5), + String uniqueTaskNamespace = 'stem:unique', + bool requireRevokeStore = false, + RetryStrategy? retryStrategy, + Iterable middleware = const [], + PayloadSigner? signer, + RoutingRegistry? routing, + TaskPayloadEncoderRegistry? encoderRegistry, + TaskPayloadEncoder resultEncoder = const JsonTaskPayloadEncoder(), + TaskPayloadEncoder argsEncoder = const JsonTaskPayloadEncoder(), + Iterable additionalEncoders = const [], + }) async { + final needsUniqueLockStore = + uniqueTasks && + uniqueTaskCoordinator == null && + workerConfig.uniqueTaskCoordinator == null; + final needsRevokeStore = + requireRevokeStore && + revokeStore == null && + workerConfig.revokeStore == null; + + final stack = StemStack.fromUrl( + url, + adapters: adapters, + overrides: overrides, + uniqueTasks: needsUniqueLockStore, + requireRevokeStore: needsRevokeStore, + ); + + final autoDisposers = Function()>[]; + + var resolvedUniqueTaskCoordinator = + uniqueTaskCoordinator ?? workerConfig.uniqueTaskCoordinator; + if (needsUniqueLockStore) { + final lockFactory = stack.lockStore; + if (lockFactory == null) { + throw StateError( + 'Unique task coordination requested but lock store factory missing.', + ); + } + final lockStore = await lockFactory.create(); + resolvedUniqueTaskCoordinator = UniqueTaskCoordinator( + lockStore: lockStore, + defaultTtl: uniqueTaskDefaultTtl, + namespace: uniqueTaskNamespace, + ); + autoDisposers.add(() async => lockFactory.dispose(lockStore)); + } + + var resolvedRevokeStore = revokeStore ?? workerConfig.revokeStore; + if (needsRevokeStore) { + final revokeFactory = stack.revokeStore; + if (revokeFactory == null) { + throw StateError('Revoke store required but no revoke factory found.'); + } + final createdRevokeStore = await revokeFactory.create(); + resolvedRevokeStore = createdRevokeStore; + autoDisposers.add(() async => revokeFactory.dispose(createdRevokeStore)); + } + + final app = await create( + tasks: tasks, + registry: registry, + broker: stack.broker, + backend: stack.backend, + workerConfig: workerConfig, + revokeStore: resolvedRevokeStore, + uniqueTaskCoordinator: resolvedUniqueTaskCoordinator, + retryStrategy: retryStrategy, + middleware: middleware, + signer: signer, + routing: routing, + encoderRegistry: encoderRegistry, + resultEncoder: resultEncoder, + argsEncoder: argsEncoder, + additionalEncoders: additionalEncoders, + ); + + if (autoDisposers.isNotEmpty) { + // Dispose auto-provisioned lock/revoke stores after worker shutdown and + // before backend/broker factories are disposed. + app._disposers.insertAll(1, autoDisposers); + } + + return app; + } + /// Creates a Stem app using a shared [StemClient]. static Future fromClient( StemClient client, { diff --git a/packages/stem/lib/src/bootstrap/stem_client.dart b/packages/stem/lib/src/bootstrap/stem_client.dart index 3da3ea9d..9405882c 100644 --- a/packages/stem/lib/src/bootstrap/stem_client.dart +++ b/packages/stem/lib/src/bootstrap/stem_client.dart @@ -1,5 +1,6 @@ import 'package:stem/src/bootstrap/factories.dart'; import 'package:stem/src/bootstrap/stem_app.dart'; +import 'package:stem/src/bootstrap/stem_stack.dart'; import 'package:stem/src/bootstrap/workflow_app.dart'; import 'package:stem/src/core/contracts.dart'; import 'package:stem/src/core/stem.dart'; @@ -75,6 +76,52 @@ abstract class StemClient { ); } + /// Creates a client from a single backend URL plus adapter wiring. + /// + /// This resolves broker/backend factories via [StemStack.fromUrl] so callers + /// can avoid manual factory wiring for common Redis/Postgres/SQLite setups. + static Future fromUrl( + String url, { + Iterable> tasks = const [], + TaskRegistry? taskRegistry, + WorkflowRegistry? workflowRegistry, + Iterable adapters = const [], + StemStoreOverrides overrides = const StemStoreOverrides(), + RoutingRegistry? routing, + RetryStrategy? retryStrategy, + UniqueTaskCoordinator? uniqueTaskCoordinator, + Iterable middleware = const [], + PayloadSigner? signer, + StemWorkerConfig defaultWorkerConfig = const StemWorkerConfig(), + TaskPayloadEncoderRegistry? encoderRegistry, + TaskPayloadEncoder resultEncoder = const JsonTaskPayloadEncoder(), + TaskPayloadEncoder argsEncoder = const JsonTaskPayloadEncoder(), + Iterable additionalEncoders = const [], + }) { + final stack = StemStack.fromUrl( + url, + adapters: adapters, + overrides: overrides, + ); + return create( + tasks: tasks, + taskRegistry: taskRegistry, + workflowRegistry: workflowRegistry, + broker: stack.broker, + backend: stack.backend, + routing: routing, + retryStrategy: retryStrategy, + uniqueTaskCoordinator: uniqueTaskCoordinator, + middleware: middleware, + signer: signer, + defaultWorkerConfig: defaultWorkerConfig, + encoderRegistry: encoderRegistry, + resultEncoder: resultEncoder, + argsEncoder: argsEncoder, + additionalEncoders: additionalEncoders, + ); + } + /// Underlying broker used by the client. Broker get broker; diff --git a/packages/stem/lib/src/bootstrap/stem_stack.dart b/packages/stem/lib/src/bootstrap/stem_stack.dart index cd461418..b401cded 100644 --- a/packages/stem/lib/src/bootstrap/stem_stack.dart +++ b/packages/stem/lib/src/bootstrap/stem_stack.dart @@ -154,6 +154,28 @@ class StemStack { final lockUri = _resolveUri(overrides.lock, baseUri); final revokeUri = _resolveUri(overrides.revoke, backendUri); + if (scheduling) { + _failIfSqliteStoreUnsupported( + kind: StemStoreKind.schedule, + uri: scheduleUri, + toggle: 'scheduling', + ); + } + if (uniqueTasks) { + _failIfSqliteStoreUnsupported( + kind: StemStoreKind.lock, + uri: lockUri, + toggle: 'uniqueTasks', + ); + } + if (requireRevokeStore) { + _failIfSqliteStoreUnsupported( + kind: StemStoreKind.revoke, + uri: revokeUri, + toggle: 'requireRevokeStore', + ); + } + final broker = _requireFactory( registered, StemStoreKind.broker, @@ -174,6 +196,7 @@ class StemStack { StemStoreKind.workflow, workflowUri, (adapter) => adapter.workflowStoreFactory(workflowUri), + toggle: 'workflows', ) : WorkflowStoreFactory.inMemory(); @@ -183,6 +206,7 @@ class StemStack { StemStoreKind.schedule, scheduleUri, (adapter) => adapter.scheduleStoreFactory(scheduleUri), + toggle: 'scheduling', ) : null; @@ -192,6 +216,7 @@ class StemStack { StemStoreKind.lock, lockUri, (adapter) => adapter.lockStoreFactory(lockUri), + toggle: 'uniqueTasks', ) : _optionalFactory( registered, @@ -206,6 +231,7 @@ class StemStack { StemStoreKind.revoke, revokeUri, (adapter) => adapter.revokeStoreFactory(revokeUri), + toggle: 'requireRevokeStore', ) : _optionalFactory( registered, @@ -285,15 +311,29 @@ T _requireFactory( Iterable adapters, StemStoreKind kind, Uri uri, - T? Function(StemStoreAdapter adapter) resolver, -) { + T? Function(StemStoreAdapter adapter) resolver, { + String? toggle, +}) { + final matched = []; for (final adapter in adapters) { if (!adapter.supports(uri, kind)) continue; + matched.add(adapter); final factory = resolver(adapter); if (factory != null) return factory; } + if (matched.isNotEmpty) { + final adapterList = matched.map((adapter) => adapter.name).join(', '); + final toggleHint = toggle == null + ? 'Use a URL/adapter that provides `${kind.name}`.' + : 'Disable `$toggle` or configure a URL override for `${kind.name}`.'; + throw StateError( + 'Adapter(s) [$adapterList] support $uri but do not provide a ' + '${kind.name} store. $toggleHint', + ); + } throw StateError( - 'No adapter registered for ${kind.name} at ${uri.scheme} ($uri).', + 'No adapter registered for ${kind.name} at ${uri.scheme} ($uri). ' + 'Register an adapter for `${uri.scheme}` or use a supported URL.', ); } @@ -310,3 +350,17 @@ T? _optionalFactory( } return null; } + +void _failIfSqliteStoreUnsupported({ + required StemStoreKind kind, + required Uri uri, + required String toggle, +}) { + final isSqlite = uri.scheme == 'sqlite' || uri.scheme == 'file'; + if (!isSqlite) return; + throw StateError( + 'sqlite URLs do not provide a ${kind.name} store in default stack ' + 'resolution. ' + 'Disable `$toggle` or configure a URL override for `${kind.name}`.', + ); +} diff --git a/packages/stem/lib/src/bootstrap/workflow_app.dart b/packages/stem/lib/src/bootstrap/workflow_app.dart index 8fefdcc9..6a0379a3 100644 --- a/packages/stem/lib/src/bootstrap/workflow_app.dart +++ b/packages/stem/lib/src/bootstrap/workflow_app.dart @@ -1,7 +1,10 @@ import 'package:stem/src/bootstrap/factories.dart'; import 'package:stem/src/bootstrap/stem_app.dart'; import 'package:stem/src/bootstrap/stem_client.dart'; +import 'package:stem/src/bootstrap/stem_stack.dart'; +import 'package:stem/src/control/revoke_store.dart'; import 'package:stem/src/core/task_payload_encoder.dart'; +import 'package:stem/src/core/unique_task_coordinator.dart'; import 'package:stem/src/workflow/core/event_bus.dart'; import 'package:stem/src/workflow/core/flow.dart'; import 'package:stem/src/workflow/core/run_state.dart'; @@ -332,6 +335,77 @@ class StemWorkflowApp { ); } + /// Creates a workflow app from a single backend URL plus adapter wiring. + /// + /// This wires broker/backend and workflow-store factories from one URL and + /// optional per-store overrides via [StemStack.fromUrl]. + static Future fromUrl( + String url, { + Iterable workflows = const [], + Iterable flows = const [], + Iterable scripts = const [], + Iterable adapters = const [], + StemStoreOverrides overrides = const StemStoreOverrides(), + StemWorkerConfig workerConfig = const StemWorkerConfig(queue: 'workflow'), + bool uniqueTasks = false, + Duration uniqueTaskDefaultTtl = const Duration(minutes: 5), + String uniqueTaskNamespace = 'stem:unique', + bool requireRevokeStore = false, + RevokeStore? revokeStore, + UniqueTaskCoordinator? uniqueTaskCoordinator, + Duration pollInterval = const Duration(milliseconds: 500), + Duration leaseExtension = const Duration(seconds: 30), + WorkflowRegistry? workflowRegistry, + WorkflowIntrospectionSink? introspectionSink, + WorkflowEventBusFactory? eventBusFactory, + TaskPayloadEncoderRegistry? encoderRegistry, + TaskPayloadEncoder resultEncoder = const JsonTaskPayloadEncoder(), + TaskPayloadEncoder argsEncoder = const JsonTaskPayloadEncoder(), + Iterable additionalEncoders = const [], + }) async { + final stack = StemStack.fromUrl( + url, + adapters: adapters, + overrides: overrides, + workflows: true, + ); + + final app = await StemApp.fromUrl( + url, + adapters: adapters, + overrides: overrides, + workerConfig: workerConfig, + uniqueTasks: uniqueTasks, + uniqueTaskDefaultTtl: uniqueTaskDefaultTtl, + uniqueTaskNamespace: uniqueTaskNamespace, + requireRevokeStore: requireRevokeStore, + revokeStore: revokeStore, + uniqueTaskCoordinator: uniqueTaskCoordinator, + encoderRegistry: encoderRegistry, + resultEncoder: resultEncoder, + argsEncoder: argsEncoder, + additionalEncoders: additionalEncoders, + ); + + return create( + workflows: workflows, + flows: flows, + scripts: scripts, + stemApp: app, + storeFactory: stack.workflowStore, + eventBusFactory: eventBusFactory, + workerConfig: workerConfig, + pollInterval: pollInterval, + leaseExtension: leaseExtension, + workflowRegistry: workflowRegistry, + introspectionSink: introspectionSink, + encoderRegistry: encoderRegistry, + resultEncoder: resultEncoder, + argsEncoder: argsEncoder, + additionalEncoders: additionalEncoders, + ); + } + /// Creates a workflow app backed by a shared [StemClient]. static Future fromClient({ required StemClient client, diff --git a/packages/stem/test/bootstrap/stem_app_test.dart b/packages/stem/test/bootstrap/stem_app_test.dart index 8f0b2980..8bf22ae3 100644 --- a/packages/stem/test/bootstrap/stem_app_test.dart +++ b/packages/stem/test/bootstrap/stem_app_test.dart @@ -119,6 +119,78 @@ void main() { await app.shutdown(); } }); + + test('fromUrl resolves adapter-backed broker/backend', () async { + final handler = FunctionTaskHandler( + name: 'test.from-url', + entrypoint: (context, args) async => null, + ); + final adapter = _BootstrapAdapter( + scheme: 'test', + broker: StemBrokerFactory(create: () async => InMemoryBroker()), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + ); + + final app = await StemApp.fromUrl( + 'test://localhost', + adapters: [adapter], + tasks: [handler], + ); + try { + await app.start(); + final taskId = await app.stem.enqueue('test.from-url'); + final completed = await app.backend + .watch(taskId) + .firstWhere((status) => status.state == TaskState.succeeded) + .timeout(const Duration(seconds: 1)); + expect(completed.state, TaskState.succeeded); + } finally { + await app.shutdown(); + } + }); + + test( + 'fromUrl auto-wires unique/revoke stores and disposes them on shutdown', + () async { + final createdLockStore = InMemoryLockStore(); + final createdRevokeStore = InMemoryRevokeStore(); + var lockDisposed = false; + var revokeDisposed = false; + final adapter = _BootstrapAdapter( + scheme: 'test', + broker: StemBrokerFactory(create: () async => InMemoryBroker()), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + lock: LockStoreFactory( + create: () async => createdLockStore, + dispose: (store) async => lockDisposed = true, + ), + revoke: RevokeStoreFactory( + create: () async => createdRevokeStore, + dispose: (store) async => revokeDisposed = true, + ), + ); + + final app = await StemApp.fromUrl( + 'test://localhost', + adapters: [adapter], + uniqueTasks: true, + requireRevokeStore: true, + ); + try { + expect(app.worker.uniqueTaskCoordinator, isNotNull); + expect(app.worker.revokeStore, same(createdRevokeStore)); + } finally { + await app.shutdown(); + } + + expect(lockDisposed, isTrue); + expect(revokeDisposed, isTrue); + }, + ); }); group('StemWorkflowApp', () { @@ -239,6 +311,41 @@ void main() { await workflowApp.shutdown(); } }); + + test('fromUrl runs workflow to completion', () async { + final flow = Flow( + name: 'workflow.from-url', + build: (builder) { + builder.step('hello', (ctx) async => 'from-url'); + }, + ); + final adapter = _BootstrapAdapter( + scheme: 'test', + broker: StemBrokerFactory(create: () async => InMemoryBroker()), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + workflow: WorkflowStoreFactory( + create: () async => InMemoryWorkflowStore(), + ), + ); + + final workflowApp = await StemWorkflowApp.fromUrl( + 'test://localhost', + adapters: [adapter], + flows: [flow], + ); + try { + final runId = await workflowApp.startWorkflow('workflow.from-url'); + final result = await workflowApp.waitForCompletion( + runId, + timeout: const Duration(seconds: 2), + ); + expect(result?.value, 'from-url'); + } finally { + await workflowApp.shutdown(); + } + }); }); } @@ -289,3 +396,45 @@ class _TestMiddleware implements Middleware { Future onExecute(TaskContext context, Future Function() next) => next(); } + +class _BootstrapAdapter implements StemStoreAdapter { + _BootstrapAdapter({ + required this.scheme, + this.broker, + this.backend, + this.workflow, + this.lock, + this.revoke, + }); + + final String scheme; + final StemBrokerFactory? broker; + final StemBackendFactory? backend; + final WorkflowStoreFactory? workflow; + final LockStoreFactory? lock; + final RevokeStoreFactory? revoke; + + @override + String get name => 'bootstrap-test-adapter'; + + @override + bool supports(Uri uri, StemStoreKind kind) => uri.scheme == scheme; + + @override + StemBrokerFactory? brokerFactory(Uri uri) => broker; + + @override + StemBackendFactory? backendFactory(Uri uri) => backend; + + @override + WorkflowStoreFactory? workflowStoreFactory(Uri uri) => workflow; + + @override + ScheduleStoreFactory? scheduleStoreFactory(Uri uri) => null; + + @override + LockStoreFactory? lockStoreFactory(Uri uri) => lock; + + @override + RevokeStoreFactory? revokeStoreFactory(Uri uri) => revoke; +} diff --git a/packages/stem/test/bootstrap/stem_client_test.dart b/packages/stem/test/bootstrap/stem_client_test.dart index cd457cc4..069ceeac 100644 --- a/packages/stem/test/bootstrap/stem_client_test.dart +++ b/packages/stem/test/bootstrap/stem_client_test.dart @@ -25,4 +25,74 @@ void main() { await app.close(); await client.close(); }); + + test('StemClient fromUrl resolves adapter-backed broker/backend', () async { + final handler = FunctionTaskHandler( + name: 'client.from-url', + entrypoint: (context, args) async => 'ok', + ); + final client = await StemClient.fromUrl( + 'test://localhost', + adapters: [ + _ClientAdapter( + scheme: 'test', + broker: StemBrokerFactory(create: () async => InMemoryBroker()), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + ), + ], + tasks: [handler], + ); + + final worker = await client.createWorker(); + await worker.start(); + try { + final taskId = await client.stem.enqueue('client.from-url'); + final result = await client.stem.waitForTask( + taskId, + timeout: const Duration(seconds: 2), + ); + expect(result?.value, 'ok'); + } finally { + await worker.shutdown(); + await client.close(); + } + }); +} + +class _ClientAdapter implements StemStoreAdapter { + _ClientAdapter({ + required this.scheme, + this.broker, + this.backend, + }); + + final String scheme; + final StemBrokerFactory? broker; + final StemBackendFactory? backend; + + @override + String get name => 'client-test-adapter'; + + @override + bool supports(Uri uri, StemStoreKind kind) => uri.scheme == scheme; + + @override + StemBrokerFactory? brokerFactory(Uri uri) => broker; + + @override + StemBackendFactory? backendFactory(Uri uri) => backend; + + @override + WorkflowStoreFactory? workflowStoreFactory(Uri uri) => null; + + @override + ScheduleStoreFactory? scheduleStoreFactory(Uri uri) => null; + + @override + LockStoreFactory? lockStoreFactory(Uri uri) => null; + + @override + RevokeStoreFactory? revokeStoreFactory(Uri uri) => null; } diff --git a/packages/stem/test/bootstrap/stem_stack_test.dart b/packages/stem/test/bootstrap/stem_stack_test.dart index b944e421..a062e402 100644 --- a/packages/stem/test/bootstrap/stem_stack_test.dart +++ b/packages/stem/test/bootstrap/stem_stack_test.dart @@ -101,7 +101,16 @@ void main() { adapters: [adapter], uniqueTasks: true, ), - throwsA(isA()), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a lock store'), + contains('Disable `uniqueTasks`'), + ), + ), + ), ); final stack = StemStack.fromUrl( @@ -127,7 +136,67 @@ void main() { adapters: [adapter], requireRevokeStore: true, ), - throwsA(isA()), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a revoke store'), + contains('Disable `requireRevokeStore`'), + ), + ), + ), + ); + }); + + test('fails clearly when workflow store is missing', () { + final adapter = _TestAdapter( + scheme: 'test', + brokerFactory: StemBrokerFactory(create: () async => InMemoryBroker()), + backendFactory: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + ); + + expect( + () => StemStack.fromUrl( + 'test://localhost', + adapters: [adapter], + workflows: true, + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a workflow store'), + contains('Disable `workflows`'), + ), + ), + ), + ); + }); + + test('fails clearly when broker is missing for a supported scheme', () { + final adapter = _TestAdapter( + scheme: 'test', + backendFactory: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + ); + + expect( + () => StemStack.fromUrl( + 'test://localhost', + adapters: [adapter], + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('do not provide a broker store'), + ), + ), ); }); @@ -151,6 +220,57 @@ void main() { expect(stack.broker, same(customBroker)); }); + + test('fails fast for sqlite scheduling store requests', () { + expect( + () => StemStack.fromUrl('sqlite:///tmp/stem.db', scheduling: true), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a schedule store'), + contains('Disable `scheduling`'), + ), + ), + ), + ); + }); + + test('fails fast for sqlite lock store requests', () { + expect( + () => StemStack.fromUrl('sqlite:///tmp/stem.db', uniqueTasks: true), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a lock store'), + contains('Disable `uniqueTasks`'), + ), + ), + ), + ); + }); + + test('fails fast for sqlite revoke store requests', () { + expect( + () => StemStack.fromUrl( + 'sqlite:///tmp/stem.db', + requireRevokeStore: true, + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a revoke store'), + contains('Disable `requireRevokeStore`'), + ), + ), + ), + ); + }); }); } diff --git a/packages/stem_adapter_tests/CHANGELOG.md b/packages/stem_adapter_tests/CHANGELOG.md index f4c52b6a..14c133be 100644 --- a/packages/stem_adapter_tests/CHANGELOG.md +++ b/packages/stem_adapter_tests/CHANGELOG.md @@ -1,5 +1,7 @@ -## 0.1.1 +## Unreleased +- Expanded adapter contract documentation with a capability matrix, explicit + skip semantics, and recipe-style setup examples. - Scoped the binary payload round-trip contract test to the Base64 encoder suite so JSON encoder runs no longer report intentional skips. diff --git a/packages/stem_adapter_tests/README.md b/packages/stem_adapter_tests/README.md index b6069c6c..7ef22f56 100644 --- a/packages/stem_adapter_tests/README.md +++ b/packages/stem_adapter_tests/README.md @@ -1,58 +1,140 @@ # stem_adapter_tests -Shared contract test suites used by Stem adapter packages to ensure brokers and -result backends satisfy the core runtime contract. Intended as a dev dependency -for packages like `stem_redis`, `stem_postgres`, and custom adapter -implementations. +Shared contract suites for adapter packages. Use this package to prove your +broker, result backend, workflow store, and lock store semantics match Stem's +runtime expectations. ## Install -Add as a dev dependency: - ```bash dart pub add --dev stem_adapter_tests ``` -Then invoke the contract suites from your package tests: +## Quick Start ```dart import 'package:stem_adapter_tests/stem_adapter_tests.dart'; void main() { runBrokerContractTests( - adapterName: 'MyBroker', + adapterName: 'my-adapter', factory: BrokerContractFactory(create: createBroker), ); - final storeFactory = WorkflowStoreContractFactory( + runResultBackendContractTests( + adapterName: 'my-adapter', + factory: ResultBackendContractFactory(create: createBackend), + ); + + final workflowFactory = WorkflowStoreContractFactory( create: createWorkflowStore, ); runWorkflowStoreContractTests( adapterName: 'my-adapter', - factory: storeFactory, + factory: workflowFactory, ); - // Facade coverage validates the high-level workflow DSL across adapters. runWorkflowScriptFacadeTests( adapterName: 'my-adapter', - factory: storeFactory, + factory: workflowFactory, ); } +``` -Future createWorkflowStore(FakeWorkflowClock clock) async { - return MyWorkflowStore(clock: clock); -} +## Capability Flags + +Capability flags let adapters opt out of specific behavior checks while keeping +all other contract assertions active. + +### BrokerContractCapabilities + +| Flag | Default | Affects | Behavior when enabled | +|---|---|---|---| +| `verifyPriorityOrdering` | `true` | Broker priority test group | Verifies higher-priority messages are delivered first. | +| `verifyBroadcastFanout` | `false` | Broadcast fan-out test group | Verifies broadcast delivery reaches all subscribers and replay semantics remain correct. | + +### ResultBackendContractCapabilities + +| Flag | Default | Affects | Behavior when enabled | +|---|---|---|---| +| `verifyTaskStatusExpiry` | `true` | Task status expiry tests | Verifies status TTL expiration behavior. | +| `verifyGroupExpiry` | `true` | Group expiry tests | Verifies group TTL expiration and post-expiry behavior. | +| `verifyChordClaiming` | `true` | Chord claiming tests | Verifies single-claimant callback dispatch semantics. | +| `verifyWorkerHeartbeats` | `true` | Heartbeat CRUD tests | Verifies heartbeat set/get/list/update behavior. | +| `verifyHeartbeatExpiry` | `true` | Heartbeat expiry tests | Verifies heartbeat TTL expiration behavior independently from heartbeat CRUD checks. | + +### WorkflowStoreContractCapabilities + +| Flag | Default | Affects | Behavior when enabled | +|---|---|---|---| +| `verifyVersionedCheckpoints` | `true` | Checkpoint versioning tests | Verifies versioned checkpoint persistence and retrieval. | +| `verifyRunLeases` | `true` | Run lease tests | Verifies claim/renew/release lease semantics. | +| `verifyWatcherRegistry` | `true` | Watcher tests | Verifies watcher registration, listing, and resolution behavior. | +| `verifyRunsWaitingOn` | `true` | Waiting-topic lookup tests | Verifies lookups for runs waiting on external topics. | +| `verifyFilteredRunListing` | `true` | Filtered run listing tests | Verifies filtered listing and pagination semantics. | + +### LockStoreContractCapabilities + +| Flag | Default | Affects | Behavior when enabled | +|---|---|---|---| +| `verifyOwnerLookup` | `true` | `ownerOf` tests | Verifies lock owner lookup behavior. | +| `verifyRenewSemantics` | `true` | Renew and expiry tests | Verifies renewal/TTL semantics for active locks. | + +## Skip Behavior + +Each flagged test uses explicit `skip` values (instead of implicit omission) so +it is always clear which capability disabled a test and why. + +## Adapter Recipes + +### Full-feature adapter + +```dart +runResultBackendContractTests( + adapterName: 'full-adapter', + factory: ResultBackendContractFactory(create: createBackend), + settings: const ResultBackendContractSettings( + capabilities: ResultBackendContractCapabilities(), + ), +); +``` + +### Adapter without broadcast fan-out + +```dart +runBrokerContractTests( + adapterName: 'queue-only-adapter', + factory: BrokerContractFactory(create: createBroker), + settings: const BrokerContractSettings( + capabilities: BrokerContractCapabilities( + verifyBroadcastFanout: false, + ), + ), +); +``` + +### Adapter with heartbeat CRUD but no heartbeat expiry + +```dart +runResultBackendContractTests( + adapterName: 'no-heartbeat-expiry-adapter', + factory: ResultBackendContractFactory(create: createBackend), + settings: const ResultBackendContractSettings( + capabilities: ResultBackendContractCapabilities( + verifyWorkerHeartbeats: true, + verifyHeartbeatExpiry: false, + ), + ), +); ``` -The workflow store suite exercises durable watcher semantics (`registerWatcher`, -`resolveWatchers`, and `listWatchers`) so adapters must capture event payloads -atomically and expose waiting runs for operator tooling. +## Workflow Clock Requirement -The factory receives a shared `FakeWorkflowClock`. Inject the same instance into -your `WorkflowRuntime` during facade tests so both the runtime and store observe -the same deterministic timeline. +Workflow store factories receive a shared `FakeWorkflowClock`. Inject that same +clock into your runtime/store under test so workflow facade and store assertions +observe the same deterministic timeline. ## Versioning -This package follows the same release cadence as the `stem` runtime. +This package tracks the same release cadence as `stem`. diff --git a/packages/stem_builder/CHANGELOG.md b/packages/stem_builder/CHANGELOG.md index dcee2077..2f143aed 100644 --- a/packages/stem_builder/CHANGELOG.md +++ b/packages/stem_builder/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.1.0 +## Unreleased - Initial builder for annotated Stem workflow/task registries. - Expanded the registry builder implementation and hardened generation output. diff --git a/packages/stem_cli/CHANGELOG.md b/packages/stem_cli/CHANGELOG.md index 5d5296ba..89377d49 100644 --- a/packages/stem_cli/CHANGELOG.md +++ b/packages/stem_cli/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- No package-specific runtime changes in this workspace update. + ## 0.1.0 - Updated CLI adapter wiring and docker test stack to the Ormed-backed @@ -5,7 +9,7 @@ - Added workflow agent help output to document required configuration. - Added cloud configuration helpers and revoke-store factory wiring. - Improved auth token handling in CLI utilities and expanded tests. -- Updated README/Justfile guidance and refreshed dependencies. +- Updated README/Taskfile guidance and refreshed dependencies. ## 0.1.0-alpha.4 diff --git a/packages/stem_memory/CHANGELOG.md b/packages/stem_memory/CHANGELOG.md index c8d5344b..308e5dbb 100644 --- a/packages/stem_memory/CHANGELOG.md +++ b/packages/stem_memory/CHANGELOG.md @@ -1,5 +1,9 @@ -## 0.1.0 +## Unreleased +- Renamed `memoryBackendFactory` to `memoryResultBackendFactory` for adapter + factory naming consistency. +- Updated docs and exports to use `StemClient`-first examples and the renamed + result backend factory. - Added `stem_memory` package with in-memory adapter exports and factory helpers. - Added shared adapter contract coverage (broker/backend/workflow/lock) for the diff --git a/packages/stem_memory/README.md b/packages/stem_memory/README.md index caec7078..3c7ee56c 100644 --- a/packages/stem_memory/README.md +++ b/packages/stem_memory/README.md @@ -11,22 +11,34 @@ dart pub add stem_memory ## Usage ```dart +import 'dart:async'; + import 'package:stem/stem.dart'; import 'package:stem_memory/stem_memory.dart'; Future main() async { - final stem = Stem( - broker: InMemoryBroker(), - registry: SimpleTaskRegistry(), - backend: InMemoryResultBackend(), + final client = await StemClient.inMemory( + tasks: [ + FunctionTaskHandler( + name: 'demo.memory', + entrypoint: (context, args) async => 'ok', + ), + ], ); + final worker = await client.createWorker(); + unawaited(worker.start()); + + final taskId = await client.stem.enqueue('demo.memory'); + print((await client.stem.waitForTask(taskId))?.value); - await stem.close(); + await worker.shutdown(); + await client.close(); } ``` ## Factories -Use `memoryBrokerFactory`, `memoryBackendFactory`, `memoryWorkflowStoreFactory`, -`memoryEventBusFactory`, `memoryScheduleStoreFactory`, `memoryLockStoreFactory`, -and `memoryRevokeStoreFactory` to integrate with bootstrap helpers. +Use `memoryBrokerFactory`, `memoryResultBackendFactory`, +`memoryWorkflowStoreFactory`, `memoryEventBusFactory`, +`memoryScheduleStoreFactory`, `memoryLockStoreFactory`, and +`memoryRevokeStoreFactory` to integrate with bootstrap helpers. diff --git a/packages/stem_memory/lib/src/memory_factories.dart b/packages/stem_memory/lib/src/memory_factories.dart index 85cb327b..16a5ebd1 100644 --- a/packages/stem_memory/lib/src/memory_factories.dart +++ b/packages/stem_memory/lib/src/memory_factories.dart @@ -21,7 +21,7 @@ StemBrokerFactory memoryBrokerFactory({ } /// Creates a [StemBackendFactory] backed by [InMemoryResultBackend]. -StemBackendFactory memoryBackendFactory({ +StemBackendFactory memoryResultBackendFactory({ Duration defaultTtl = const Duration(days: 1), Duration groupDefaultTtl = const Duration(days: 1), Duration heartbeatTtl = const Duration(minutes: 1), diff --git a/packages/stem_memory/lib/stem_memory.dart b/packages/stem_memory/lib/stem_memory.dart index d8f1b8d7..b6d988de 100644 --- a/packages/stem_memory/lib/stem_memory.dart +++ b/packages/stem_memory/lib/stem_memory.dart @@ -13,10 +13,10 @@ export 'package:stem/stem.dart' StemMemoryAdapter; export 'src/memory_factories.dart' show - memoryBackendFactory, memoryBrokerFactory, memoryEventBusFactory, memoryLockStoreFactory, + memoryResultBackendFactory, memoryRevokeStoreFactory, memoryScheduleStoreFactory, memoryWorkflowStoreFactory; diff --git a/packages/stem_postgres/CHANGELOG.md b/packages/stem_postgres/CHANGELOG.md index 67a73e02..09fe12ad 100644 --- a/packages/stem_postgres/CHANGELOG.md +++ b/packages/stem_postgres/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +- Normalized `postgresResultBackendFactory` to accept a positional `uri` + argument, matching the adapter factory style used across packages. +- Updated Postgres adapter wiring to use the new factory signature. + ## 0.1.0 - Added workflow run lease tracking and claim/renew helpers to distribute diff --git a/packages/stem_postgres/lib/src/stack/postgres_adapter.dart b/packages/stem_postgres/lib/src/stack/postgres_adapter.dart index 1d5c73a0..8a63bbcf 100644 --- a/packages/stem_postgres/lib/src/stack/postgres_adapter.dart +++ b/packages/stem_postgres/lib/src/stack/postgres_adapter.dart @@ -131,7 +131,7 @@ class StemPostgresAdapter implements StemStoreAdapter { @override StemBackendFactory? backendFactory(Uri uri) { return postgresResultBackendFactory( - connectionString: uri.toString(), + uri.toString(), namespace: namespace, defaultTtl: backendDefaultTtl, groupDefaultTtl: backendGroupDefaultTtl, diff --git a/packages/stem_postgres/lib/src/workflow/postgres_factories.dart b/packages/stem_postgres/lib/src/workflow/postgres_factories.dart index 2e5f85d7..4c285669 100644 --- a/packages/stem_postgres/lib/src/workflow/postgres_factories.dart +++ b/packages/stem_postgres/lib/src/workflow/postgres_factories.dart @@ -26,8 +26,8 @@ StemBrokerFactory postgresBrokerFactory( } /// Creates a [StemBackendFactory] backed by PostgreSQL. -StemBackendFactory postgresResultBackendFactory({ - String? connectionString, +StemBackendFactory postgresResultBackendFactory( + String uri, { String namespace = 'stem', Duration defaultTtl = const Duration(days: 1), Duration groupDefaultTtl = const Duration(days: 1), @@ -35,7 +35,7 @@ StemBackendFactory postgresResultBackendFactory({ }) { return StemBackendFactory( create: () async => PostgresResultBackend.connect( - connectionString: connectionString, + connectionString: uri, namespace: namespace, defaultTtl: defaultTtl, groupDefaultTtl: groupDefaultTtl, diff --git a/packages/stem_redis/CHANGELOG.md b/packages/stem_redis/CHANGELOG.md index 2e5081e4..80f55210 100644 --- a/packages/stem_redis/CHANGELOG.md +++ b/packages/stem_redis/CHANGELOG.md @@ -1,6 +1,6 @@ -## 0.1.1 +## Unreleased - Enabled broadcast fan-out broker contract coverage in Redis integration tests by wiring additional broker instances for shared-namespace fan-out checks. diff --git a/packages/stem_sqlite/CHANGELOG.md b/packages/stem_sqlite/CHANGELOG.md index 8a98a164..f5af342b 100644 --- a/packages/stem_sqlite/CHANGELOG.md +++ b/packages/stem_sqlite/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.1.1 +## Unreleased - Added broker broadcast fan-out support for SQLite routing subscriptions with broadcast channels. From f4a4cc01bd8d035a15609c5f89e6e0b833301cf6 Mon Sep 17 00:00:00 2001 From: kingwill101 Date: Tue, 24 Feb 2026 08:25:59 -0500 Subject: [PATCH 2/4] fix(bootstrap): address PR review resource and adapter edge cases --- packages/stem/lib/src/bootstrap/stem_app.dart | 59 +++++++----- .../stem/lib/src/bootstrap/stem_stack.dart | 33 ++++++- .../stem/lib/src/bootstrap/workflow_app.dart | 45 +++++---- .../stem/test/bootstrap/stem_app_test.dart | 93 +++++++++++++++++++ .../stem/test/bootstrap/stem_stack_test.dart | 44 +++++++++ 5 files changed, 231 insertions(+), 43 deletions(-) diff --git a/packages/stem/lib/src/bootstrap/stem_app.dart b/packages/stem/lib/src/bootstrap/stem_app.dart index 5e15c1d4..b2f511e1 100644 --- a/packages/stem/lib/src/bootstrap/stem_app.dart +++ b/packages/stem/lib/src/bootstrap/stem_app.dart @@ -277,31 +277,44 @@ class StemApp { autoDisposers.add(() async => revokeFactory.dispose(createdRevokeStore)); } - final app = await create( - tasks: tasks, - registry: registry, - broker: stack.broker, - backend: stack.backend, - workerConfig: workerConfig, - revokeStore: resolvedRevokeStore, - uniqueTaskCoordinator: resolvedUniqueTaskCoordinator, - retryStrategy: retryStrategy, - middleware: middleware, - signer: signer, - routing: routing, - encoderRegistry: encoderRegistry, - resultEncoder: resultEncoder, - argsEncoder: argsEncoder, - additionalEncoders: additionalEncoders, - ); + try { + final app = await create( + tasks: tasks, + registry: registry, + broker: stack.broker, + backend: stack.backend, + workerConfig: workerConfig, + revokeStore: resolvedRevokeStore, + uniqueTaskCoordinator: resolvedUniqueTaskCoordinator, + retryStrategy: retryStrategy, + middleware: middleware, + signer: signer, + routing: routing, + encoderRegistry: encoderRegistry, + resultEncoder: resultEncoder, + argsEncoder: argsEncoder, + additionalEncoders: additionalEncoders, + ); - if (autoDisposers.isNotEmpty) { - // Dispose auto-provisioned lock/revoke stores after worker shutdown and - // before backend/broker factories are disposed. - app._disposers.insertAll(1, autoDisposers); - } + if (autoDisposers.isNotEmpty) { + // Dispose auto-provisioned lock/revoke stores after worker shutdown and + // before backend/broker factories are disposed. + app._disposers.insertAll(1, autoDisposers); + } - return app; + return app; + } on Object catch (error, stack) { + // If app creation fails, release any auto-provisioned stores now to avoid + // leaking startup resources. + for (final disposer in autoDisposers.reversed) { + try { + await disposer(); + } on Object { + // Keep the original startup error as the primary failure. + } + } + Error.throwWithStackTrace(error, stack); + } } /// Creates a Stem app using a shared [StemClient]. diff --git a/packages/stem/lib/src/bootstrap/stem_stack.dart b/packages/stem/lib/src/bootstrap/stem_stack.dart index b401cded..ce91b194 100644 --- a/packages/stem/lib/src/bootstrap/stem_stack.dart +++ b/packages/stem/lib/src/bootstrap/stem_stack.dart @@ -154,21 +154,39 @@ class StemStack { final lockUri = _resolveUri(overrides.lock, baseUri); final revokeUri = _resolveUri(overrides.revoke, backendUri); - if (scheduling) { + if (scheduling && + !_hasResolvedFactory( + registered, + StemStoreKind.schedule, + scheduleUri, + (adapter) => adapter.scheduleStoreFactory(scheduleUri), + )) { _failIfSqliteStoreUnsupported( kind: StemStoreKind.schedule, uri: scheduleUri, toggle: 'scheduling', ); } - if (uniqueTasks) { + if (uniqueTasks && + !_hasResolvedFactory( + registered, + StemStoreKind.lock, + lockUri, + (adapter) => adapter.lockStoreFactory(lockUri), + )) { _failIfSqliteStoreUnsupported( kind: StemStoreKind.lock, uri: lockUri, toggle: 'uniqueTasks', ); } - if (requireRevokeStore) { + if (requireRevokeStore && + !_hasResolvedFactory( + registered, + StemStoreKind.revoke, + revokeUri, + (adapter) => adapter.revokeStoreFactory(revokeUri), + )) { _failIfSqliteStoreUnsupported( kind: StemStoreKind.revoke, uri: revokeUri, @@ -351,6 +369,15 @@ T? _optionalFactory( return null; } +bool _hasResolvedFactory( + Iterable adapters, + StemStoreKind kind, + Uri uri, + T? Function(StemStoreAdapter adapter) resolver, +) { + return _optionalFactory(adapters, kind, uri, resolver) != null; +} + void _failIfSqliteStoreUnsupported({ required StemStoreKind kind, required Uri uri, diff --git a/packages/stem/lib/src/bootstrap/workflow_app.dart b/packages/stem/lib/src/bootstrap/workflow_app.dart index 6a0379a3..c5701185 100644 --- a/packages/stem/lib/src/bootstrap/workflow_app.dart +++ b/packages/stem/lib/src/bootstrap/workflow_app.dart @@ -387,23 +387,34 @@ class StemWorkflowApp { additionalEncoders: additionalEncoders, ); - return create( - workflows: workflows, - flows: flows, - scripts: scripts, - stemApp: app, - storeFactory: stack.workflowStore, - eventBusFactory: eventBusFactory, - workerConfig: workerConfig, - pollInterval: pollInterval, - leaseExtension: leaseExtension, - workflowRegistry: workflowRegistry, - introspectionSink: introspectionSink, - encoderRegistry: encoderRegistry, - resultEncoder: resultEncoder, - argsEncoder: argsEncoder, - additionalEncoders: additionalEncoders, - ); + try { + return await create( + workflows: workflows, + flows: flows, + scripts: scripts, + stemApp: app, + storeFactory: stack.workflowStore, + eventBusFactory: eventBusFactory, + workerConfig: workerConfig, + pollInterval: pollInterval, + leaseExtension: leaseExtension, + workflowRegistry: workflowRegistry, + introspectionSink: introspectionSink, + encoderRegistry: encoderRegistry, + resultEncoder: resultEncoder, + argsEncoder: argsEncoder, + additionalEncoders: additionalEncoders, + ); + } on Object catch (error, stack) { + // fromUrl owns the app instance; clean it up when workflow bootstrap + // fails. + try { + await app.shutdown(); + } on Object { + // Keep the original bootstrap failure as the primary error. + } + Error.throwWithStackTrace(error, stack); + } } /// Creates a workflow app backed by a shared [StemClient]. diff --git a/packages/stem/test/bootstrap/stem_app_test.dart b/packages/stem/test/bootstrap/stem_app_test.dart index 8bf22ae3..a60319cd 100644 --- a/packages/stem/test/bootstrap/stem_app_test.dart +++ b/packages/stem/test/bootstrap/stem_app_test.dart @@ -191,6 +191,51 @@ void main() { expect(revokeDisposed, isTrue); }, ); + + test( + 'fromUrl disposes auto-wired stores when app bootstrap fails', + () async { + final createdLockStore = InMemoryLockStore(); + final createdRevokeStore = InMemoryRevokeStore(); + var lockDisposed = false; + var revokeDisposed = false; + final adapter = _BootstrapAdapter( + scheme: 'test', + broker: StemBrokerFactory( + create: () async => throw StateError('broker bootstrap failure'), + ), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + lock: LockStoreFactory( + create: () async => createdLockStore, + dispose: (store) async => lockDisposed = true, + ), + revoke: RevokeStoreFactory( + create: () async => createdRevokeStore, + dispose: (store) async => revokeDisposed = true, + ), + ); + + await expectLater( + () => StemApp.fromUrl( + 'test://localhost', + adapters: [adapter], + uniqueTasks: true, + requireRevokeStore: true, + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('broker bootstrap failure'), + ), + ), + ); + + expect(lockDisposed, isTrue); + expect(revokeDisposed, isTrue); + }); }); group('StemWorkflowApp', () { @@ -346,6 +391,54 @@ void main() { await workflowApp.shutdown(); } }); + + test('fromUrl shuts down app when workflow bootstrap fails', () async { + final createdLockStore = InMemoryLockStore(); + final createdRevokeStore = InMemoryRevokeStore(); + var lockDisposed = false; + var revokeDisposed = false; + final adapter = _BootstrapAdapter( + scheme: 'test', + broker: StemBrokerFactory(create: () async => InMemoryBroker()), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + workflow: WorkflowStoreFactory( + create: () async => InMemoryWorkflowStore(), + ), + lock: LockStoreFactory( + create: () async => createdLockStore, + dispose: (store) async => lockDisposed = true, + ), + revoke: RevokeStoreFactory( + create: () async => createdRevokeStore, + dispose: (store) async => revokeDisposed = true, + ), + ); + + await expectLater( + () => StemWorkflowApp.fromUrl( + 'test://localhost', + adapters: [adapter], + uniqueTasks: true, + requireRevokeStore: true, + eventBusFactory: WorkflowEventBusFactory( + create: (store) async => + throw StateError('event bus bootstrap failure'), + ), + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('event bus bootstrap failure'), + ), + ), + ); + + expect(lockDisposed, isTrue); + expect(revokeDisposed, isTrue); + }); }); } diff --git a/packages/stem/test/bootstrap/stem_stack_test.dart b/packages/stem/test/bootstrap/stem_stack_test.dart index a062e402..a4691f65 100644 --- a/packages/stem/test/bootstrap/stem_stack_test.dart +++ b/packages/stem/test/bootstrap/stem_stack_test.dart @@ -271,6 +271,50 @@ void main() { ), ); }); + + test( + 'accepts sqlite stores when custom adapters provide toggle factories', + () { + final brokerFactory = StemBrokerFactory( + create: () async => InMemoryBroker(), + ); + final backendFactory = StemBackendFactory( + create: () async => InMemoryResultBackend(), + ); + final scheduleFactory = ScheduleStoreFactory( + create: () async => InMemoryScheduleStore(), + ); + final lockFactory = LockStoreFactory( + create: () async => InMemoryLockStore(), + ); + final revokeFactory = RevokeStoreFactory( + create: () async => InMemoryRevokeStore(), + ); + + final adapter = _TestAdapter( + scheme: 'sqlite', + brokerFactory: brokerFactory, + backendFactory: backendFactory, + scheduleStoreFactory: scheduleFactory, + lockStoreFactory: lockFactory, + revokeStoreFactory: revokeFactory, + ); + + final stack = StemStack.fromUrl( + 'sqlite:///tmp/stem.db', + adapters: [adapter], + scheduling: true, + uniqueTasks: true, + requireRevokeStore: true, + ); + + expect(stack.broker, same(brokerFactory)); + expect(stack.backend, same(backendFactory)); + expect(stack.scheduleStore, same(scheduleFactory)); + expect(stack.lockStore, same(lockFactory)); + expect(stack.revokeStore, same(revokeFactory)); + }, + ); }); } From 2db6be6de0fc657e895f52db12e66b4f4da68858 Mon Sep 17 00:00:00 2001 From: kingwill101 Date: Tue, 24 Feb 2026 08:45:09 -0500 Subject: [PATCH 3/4] chore: address remaining PR review concerns --- packages/dashboard/CHANGELOG.md | 4 +- packages/stem/CHANGELOG.md | 4 +- .../stem/example/autoscaling_demo/README.md | 3 - .../stem/example/canvas_patterns/Taskfile.yml | 49 +++---- .../example/docs_snippets/lib/workflows.dart | 2 +- .../example/encrypted_payload/Taskfile.yml | 97 ++++++------- packages/stem/example/microservice/README.md | 1 + .../stem/example/microservice/Taskfile.yml | 16 ++- .../example/monolith_service/Taskfile.yml | 10 +- .../stem/example/postgres_tls/Taskfile.yml | 8 +- .../stem/example/progress_heartbeat/README.md | 6 +- .../example/progress_heartbeat/Taskfile.yml | 18 ++- .../example/rate_limit_delay/Taskfile.yml | 18 ++- .../example/security/ed25519_tls/Taskfile.yml | 4 +- .../stem/example/security/hmac/Taskfile.yml | 8 +- .../stem/example/security/hmac_tls/README.md | 4 +- .../example/security/hmac_tls/Taskfile.yml | 9 +- .../example/signing_key_rotation/Taskfile.yml | 8 +- .../stem/example/task_context_mixed/README.md | 2 - .../example/task_context_mixed/Taskfile.yml | 65 +++++---- .../stem/example/unique_tasks/Taskfile.yml | 10 +- packages/stem/lib/src/bootstrap/stem_app.dart | 47 +++--- .../stem/lib/src/bootstrap/stem_stack.dart | 89 ++++++------ .../stem/lib/src/bootstrap/workflow_app.dart | 9 +- .../stem/test/bootstrap/stem_app_test.dart | 134 +++++++----------- .../stem/test/bootstrap/stem_client_test.dart | 41 +----- .../stem/test/bootstrap/stem_stack_test.dart | 88 +++++++----- .../test/bootstrap/test_store_adapter.dart | 47 ++++++ packages/stem_adapter_tests/CHANGELOG.md | 4 +- packages/stem_builder/CHANGELOG.md | 4 +- packages/stem_cli/CHANGELOG.md | 4 +- packages/stem_memory/CHANGELOG.md | 4 +- packages/stem_postgres/CHANGELOG.md | 7 +- packages/stem_redis/CHANGELOG.md | 4 +- packages/stem_sqlite/CHANGELOG.md | 4 +- 35 files changed, 452 insertions(+), 380 deletions(-) create mode 100644 packages/stem/test/bootstrap/test_store_adapter.dart diff --git a/packages/dashboard/CHANGELOG.md b/packages/dashboard/CHANGELOG.md index c45beeb9..9b244c58 100644 --- a/packages/dashboard/CHANGELOG.md +++ b/packages/dashboard/CHANGELOG.md @@ -1,3 +1,5 @@ -## Unreleased +# Changelog + +## 0.1.0 - Initial release of the `stem_dashboard` package. diff --git a/packages/stem/CHANGELOG.md b/packages/stem/CHANGELOG.md index df77e2b9..f3e05269 100644 --- a/packages/stem/CHANGELOG.md +++ b/packages/stem/CHANGELOG.md @@ -1,4 +1,6 @@ -## Unreleased +# Changelog + +## 0.1.1 - Improved bootstrap DX with explicit fail-fast errors across broker/backend/ workflow/schedule/lock/revoke resolution paths in `StemStack.fromUrl`, diff --git a/packages/stem/example/autoscaling_demo/README.md b/packages/stem/example/autoscaling_demo/README.md index b204facd..919bd62c 100644 --- a/packages/stem/example/autoscaling_demo/README.md +++ b/packages/stem/example/autoscaling_demo/README.md @@ -23,9 +23,6 @@ task build # In separate terminals: task run-worker task run-producer - -# Or use tmux: -task tmux ``` You should see log lines like: diff --git a/packages/stem/example/canvas_patterns/Taskfile.yml b/packages/stem/example/canvas_patterns/Taskfile.yml index 30e92754..22e53cd4 100644 --- a/packages/stem/example/canvas_patterns/Taskfile.yml +++ b/packages/stem/example/canvas_patterns/Taskfile.yml @@ -6,8 +6,8 @@ tasks: cmds: - echo "No build step required." - run-chain: - desc: Run chain canvas demo. + _run-demo: + internal: true cmds: - | bash -lc ' @@ -15,38 +15,39 @@ tasks: root="{{.TASKFILE_DIR}}" stem_root="$root/../.." env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + if [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + elif [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + fi cd "$stem_root" - dart run example/canvas_patterns/chain_example.dart + dart run "{{.DEMO_FILE}}" ' + run-chain: + desc: Run chain canvas demo. + cmds: + - task: _run-demo + vars: + DEMO_FILE: example/canvas_patterns/chain_example.dart + run-chord: desc: Run chord canvas demo. cmds: - - | - bash -lc ' - set -euo pipefail - root="{{.TASKFILE_DIR}}" - stem_root="$root/../.." - env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi - cd "$stem_root" - dart run example/canvas_patterns/chord_example.dart - ' + - task: _run-demo + vars: + DEMO_FILE: example/canvas_patterns/chord_example.dart run-group: desc: Run group canvas demo. cmds: - - | - bash -lc ' - set -euo pipefail - root="{{.TASKFILE_DIR}}" - stem_root="$root/../.." - env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi - cd "$stem_root" - dart run example/canvas_patterns/group_example.dart - ' + - task: _run-demo + vars: + DEMO_FILE: example/canvas_patterns/group_example.dart run: desc: Run chain demo. diff --git a/packages/stem/example/docs_snippets/lib/workflows.dart b/packages/stem/example/docs_snippets/lib/workflows.dart index 7097c412..6cb7cc79 100644 --- a/packages/stem/example/docs_snippets/lib/workflows.dart +++ b/packages/stem/example/docs_snippets/lib/workflows.dart @@ -13,7 +13,7 @@ Future bootstrapWorkflowRuntime() async { adapters: const [StemRedisAdapter(), StemPostgresAdapter()], overrides: const StemStoreOverrides( backend: 'redis://127.0.0.1:56379/1', - workflow: 'postgresql://postgres:postgres@127.0.0.1:65432/stem', + workflow: 'postgresql://:@127.0.0.1:65432/stem', ), flows: [ApprovalsFlow.flow], scripts: [retryScript], diff --git a/packages/stem/example/encrypted_payload/Taskfile.yml b/packages/stem/example/encrypted_payload/Taskfile.yml index d149bf75..1d0e829b 100644 --- a/packages/stem/example/encrypted_payload/Taskfile.yml +++ b/packages/stem/example/encrypted_payload/Taskfile.yml @@ -16,7 +16,9 @@ tasks: secret="$(openssl rand -base64 32)" escaped_secret="$(printf "%s" "$secret" | sed -e "s/[&|]/\\\\&/g")" if grep -q "^PAYLOAD_SECRET=" "$env_file"; then - sed -i -E "s|^PAYLOAD_SECRET=.*|PAYLOAD_SECRET=${escaped_secret}|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^PAYLOAD_SECRET=.*|PAYLOAD_SECRET=${escaped_secret}|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "PAYLOAD_SECRET=${secret}" >> "$env_file" fi @@ -86,83 +88,74 @@ tasks: desc: Compile worker, enqueuer, and container client binaries. deps: [build:worker, build:enqueuer, build:container-client] - run:worker: - desc: Run compiled encrypted worker locally. - deps: [build:worker] + _run:local: + internal: true cmds: - | bash -lc ' set -euo pipefail root="{{.TASKFILE_DIR}}" env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then + if [[ -f "$root/$env_file" ]]; then set -a - source "$env_file" + source "$root/$env_file" set +a - elif [[ -f "$root/$env_file" ]]; then + elif [[ -f "$env_file" ]]; then set -a - source "$root/$env_file" + source "$env_file" set +a fi export REDIS_PORT="${REDIS_PORT:-6379}" export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}" - export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}" - "$root/worker/build/bundle/bin/"* + if [[ -z "${PAYLOAD_SECRET:-}" ]]; then + echo "PAYLOAD_SECRET is not set. Run: task init" >&2 + exit 1 + fi + + if [[ -n "{{.BINARY_FILE}}" ]]; then + "$root/{{.BINARY_FILE}}" + exit 0 + fi + + shopt -s nullglob + bin_pattern="$root/{{.BINARY_GLOB}}" + bin_candidates=( $bin_pattern ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one binary matching $root/{{.BINARY_GLOB}}, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" ' + run:worker: + desc: Run compiled encrypted worker locally. + deps: [build:worker] + cmds: + - task: _run:local + vars: + BINARY_GLOB: worker/build/bundle/bin/* + BINARY_FILE: "" + run:enqueuer: desc: Run compiled encrypted enqueuer locally. deps: [build:enqueuer] cmds: - - | - bash -lc ' - set -euo pipefail - root="{{.TASKFILE_DIR}}" - env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then - set -a - source "$env_file" - set +a - elif [[ -f "$root/$env_file" ]]; then - set -a - source "$root/$env_file" - set +a - fi - export REDIS_PORT="${REDIS_PORT:-6379}" - export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" - export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" - export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}" - export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}" - "$root/enqueuer/build/bundle/bin/"* - ' + - task: _run:local + vars: + BINARY_GLOB: enqueuer/build/bundle/bin/* + BINARY_FILE: "" run:container-client: desc: Run compiled standalone encrypted container client locally. deps: [build:container-client] cmds: - - | - bash -lc ' - set -euo pipefail - root="{{.TASKFILE_DIR}}" - env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then - set -a - source "$env_file" - set +a - elif [[ -f "$root/$env_file" ]]; then - set -a - source "$root/$env_file" - set +a - fi - export REDIS_PORT="${REDIS_PORT:-6379}" - export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" - export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" - export STEM_DEFAULT_QUEUE="${STEM_DEFAULT_QUEUE:-secure}" - export PAYLOAD_SECRET="${PAYLOAD_SECRET:-kMsrVb6yQZ1w3+MCb8n2kgP0hYt1M6n1n1VQpFaFQZ8=}" - "$root/build/container_mixed_encrypted" - ' + - task: _run:local + vars: + BINARY_GLOB: "" + BINARY_FILE: build/container_mixed_encrypted clean: desc: Remove compiled artifacts. diff --git a/packages/stem/example/microservice/README.md b/packages/stem/example/microservice/README.md index d78fc6a0..73b104f1 100644 --- a/packages/stem/example/microservice/README.md +++ b/packages/stem/example/microservice/README.md @@ -40,6 +40,7 @@ Generate a fresh signing secret before production use: openssl rand -base64 32 # or run `task tls:certs` to create TLS assets as well (optional) ``` + Replace the placeholder secret in `.env` with the generated value and update `STEM_SIGNING_ACTIVE_KEY` when rotating keys. To migrate to Ed25519 signing (public/private), run: diff --git a/packages/stem/example/microservice/Taskfile.yml b/packages/stem/example/microservice/Taskfile.yml index 96d5a795..0c4ca1f5 100644 --- a/packages/stem/example/microservice/Taskfile.yml +++ b/packages/stem/example/microservice/Taskfile.yml @@ -36,12 +36,16 @@ tasks: secret="$(openssl rand -base64 32)" escaped_secret="$(printf "%s" "$secret" | sed -e "s/[&|]/\\\\&/g")" if grep -q "^STEM_SIGNING_KEYS=" "$env_file"; then - sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "STEM_SIGNING_KEYS=primary:${secret}" >> "$env_file" fi if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$env_file"; then - sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "STEM_SIGNING_ACTIVE_KEY=primary" >> "$env_file" fi @@ -68,7 +72,9 @@ tasks: value="${line#*=}" escaped_value="$(printf "%s" "$value" | sed -e "s/[&|]/\\\\&/g")" if grep -q "^${key}=" "$env_file"; then - sed -i -E "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "$key=$value" >> "$env_file" fi @@ -157,7 +163,7 @@ tasks: ' build: - desc: Compile worker, enqueuer, and beat binaries. + desc: Compile worker, enqueuer, and beat binaries (`build:dashboard` is separate). deps: [build:worker, build:enqueuer, build:beat] run:worker: @@ -263,4 +269,4 @@ tasks: clean: desc: Remove compiled build artifacts. cmds: - - rm -rf "{{.TASKFILE_DIR}}/worker/build" "{{.TASKFILE_DIR}}/enqueuer/build" "{{.TASKFILE_DIR}}/beat/build" "{{.TASKFILE_DIR}}/../../../dashboard/build" + - rm -rf "{{.TASKFILE_DIR}}/worker/build" "{{.TASKFILE_DIR}}/enqueuer/build" "{{.TASKFILE_DIR}}/beat/build" diff --git a/packages/stem/example/monolith_service/Taskfile.yml b/packages/stem/example/monolith_service/Taskfile.yml index 0327fbb2..5d75a54a 100644 --- a/packages/stem/example/monolith_service/Taskfile.yml +++ b/packages/stem/example/monolith_service/Taskfile.yml @@ -15,7 +15,15 @@ tasks: set -euo pipefail root="{{.TASKFILE_DIR}}" env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + if [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + elif [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + fi cd "$root" dart run bin/service.dart ' diff --git a/packages/stem/example/postgres_tls/Taskfile.yml b/packages/stem/example/postgres_tls/Taskfile.yml index 4df0fbfe..e2789e02 100644 --- a/packages/stem/example/postgres_tls/Taskfile.yml +++ b/packages/stem/example/postgres_tls/Taskfile.yml @@ -69,8 +69,10 @@ tasks: fi export REDIS_PORT="${REDIS_PORT:-6379}" export POSTGRES_PORT="${POSTGRES_PORT:-5432}" + export POSTGRES_USER="${POSTGRES_USER:-postgres}" + export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-postgres}" export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://127.0.0.1:${REDIS_PORT}}" - export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-postgresql://postgres:postgres@127.0.0.1:${POSTGRES_PORT}/stem_test}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${POSTGRES_PORT}/stem_test}" export STEM_TLS_CA_CERT="${STEM_TLS_CA_CERT:-$root/../../../stem_cli/docker/testing/certs/postgres-root.crt}" "$root/build/worker/bundle/bin/"* ' @@ -95,8 +97,10 @@ tasks: fi export REDIS_PORT="${REDIS_PORT:-6379}" export POSTGRES_PORT="${POSTGRES_PORT:-5432}" + export POSTGRES_USER="${POSTGRES_USER:-postgres}" + export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-postgres}" export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://127.0.0.1:${REDIS_PORT}}" - export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-postgresql://postgres:postgres@127.0.0.1:${POSTGRES_PORT}/stem_test}" + export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${POSTGRES_PORT}/stem_test}" export STEM_TLS_CA_CERT="${STEM_TLS_CA_CERT:-$root/../../../stem_cli/docker/testing/certs/postgres-root.crt}" "$root/build/enqueue/bundle/bin/"* ' diff --git a/packages/stem/example/progress_heartbeat/README.md b/packages/stem/example/progress_heartbeat/README.md index 41d2a466..d061ef97 100644 --- a/packages/stem/example/progress_heartbeat/README.md +++ b/packages/stem/example/progress_heartbeat/README.md @@ -36,8 +36,7 @@ From the host, point the CLI at Redis and query worker snapshots: export STEM_BROKER_URL=redis://localhost:6379/0 export STEM_RESULT_BACKEND_URL=redis://localhost:6379/1 -task build-cli -task stem observe workers +stem observe workers ``` The output includes the worker ID, active count, and the last heartbeat time. @@ -47,12 +46,9 @@ The output includes the worker ID, active count, and the last heartbeat time. ```bash task deps-up task build -task build-cli # In separate terminals: task run-worker task run-producer -# Or: -task tmux ``` ## Notes diff --git a/packages/stem/example/progress_heartbeat/Taskfile.yml b/packages/stem/example/progress_heartbeat/Taskfile.yml index ac72a171..6e245fe6 100644 --- a/packages/stem/example/progress_heartbeat/Taskfile.yml +++ b/packages/stem/example/progress_heartbeat/Taskfile.yml @@ -41,7 +41,14 @@ tasks: export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" export WORKER_NAME="${WORKER_NAME:-progress-worker}" - "$root/build/worker/bundle/bin/"* + shopt -s nullglob + bin_candidates=( "$root/build/worker/bundle/bin/"* ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one worker binary in $root/build/worker/bundle/bin, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" ' run-producer: @@ -57,7 +64,14 @@ tasks: export REDIS_PORT="${REDIS_PORT:-6379}" export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" - "$root/build/producer/bundle/bin/"* + shopt -s nullglob + bin_candidates=( "$root/build/producer/bundle/bin/"* ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one producer binary in $root/build/producer/bundle/bin, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" ' run: diff --git a/packages/stem/example/rate_limit_delay/Taskfile.yml b/packages/stem/example/rate_limit_delay/Taskfile.yml index a27a1561..d6299372 100644 --- a/packages/stem/example/rate_limit_delay/Taskfile.yml +++ b/packages/stem/example/rate_limit_delay/Taskfile.yml @@ -41,7 +41,14 @@ tasks: export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" export STEM_RATE_LIMIT_URL="${STEM_RATE_LIMIT_URL:-redis://localhost:${REDIS_PORT}/2}" - "$root/build/worker/bundle/bin/"* + shopt -s nullglob + bin_candidates=( "$root/build/worker/bundle/bin/"* ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one worker binary in $root/build/worker/bundle/bin, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" ' run-producer: @@ -57,7 +64,14 @@ tasks: export REDIS_PORT="${REDIS_PORT:-6379}" export STEM_BROKER_URL="${STEM_BROKER_URL:-redis://localhost:${REDIS_PORT}/0}" export STEM_RESULT_BACKEND_URL="${STEM_RESULT_BACKEND_URL:-redis://localhost:${REDIS_PORT}/1}" - "$root/build/producer/bundle/bin/"* + shopt -s nullglob + bin_candidates=( "$root/build/producer/bundle/bin/"* ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one producer binary in $root/build/producer/bundle/bin, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" ' run: diff --git a/packages/stem/example/security/ed25519_tls/Taskfile.yml b/packages/stem/example/security/ed25519_tls/Taskfile.yml index 867cd0dd..ea0928f2 100644 --- a/packages/stem/example/security/ed25519_tls/Taskfile.yml +++ b/packages/stem/example/security/ed25519_tls/Taskfile.yml @@ -29,7 +29,9 @@ tasks: value="${line#*=}" escaped_value="$(printf "%s" "$value" | sed -e "s/[&|]/\\\\&/g")" if grep -q "^${key}=" "$env_file"; then - sed -i -E "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "${key}=${value}" >> "$env_file" fi diff --git a/packages/stem/example/security/hmac/Taskfile.yml b/packages/stem/example/security/hmac/Taskfile.yml index f99443b2..9c78fd25 100644 --- a/packages/stem/example/security/hmac/Taskfile.yml +++ b/packages/stem/example/security/hmac/Taskfile.yml @@ -15,12 +15,16 @@ tasks: continue fi if grep -q "^STEM_SIGNING_KEYS=" "$env_file"; then - sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "STEM_SIGNING_KEYS=primary:${secret}" >> "$env_file" fi if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$env_file"; then - sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "STEM_SIGNING_ACTIVE_KEY=primary" >> "$env_file" fi diff --git a/packages/stem/example/security/hmac_tls/README.md b/packages/stem/example/security/hmac_tls/README.md index 13f96fb1..54964da4 100644 --- a/packages/stem/example/security/hmac_tls/README.md +++ b/packages/stem/example/security/hmac_tls/README.md @@ -7,14 +7,14 @@ This variant mirrors `examples/microservice` but adds TLS encryption for Redis w Generate self-signed certificates (or provide your own) before starting: ```bash -cd examples/security/hmac_tls +cd example/security/hmac_tls task tls:certs ``` ## Usage ```bash -cd examples/security/hmac_tls +cd example/security/hmac_tls docker compose up --build ``` diff --git a/packages/stem/example/security/hmac_tls/Taskfile.yml b/packages/stem/example/security/hmac_tls/Taskfile.yml index a4f4fb38..bcd0f304 100644 --- a/packages/stem/example/security/hmac_tls/Taskfile.yml +++ b/packages/stem/example/security/hmac_tls/Taskfile.yml @@ -26,12 +26,16 @@ tasks: continue fi if grep -q "^STEM_SIGNING_KEYS=" "$env_file"; then - sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=primary:${escaped_secret}|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "STEM_SIGNING_KEYS=primary:${secret}" >> "$env_file" fi if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$env_file"; then - sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=primary|" "$env_file" > "$tmp_file" + mv "$tmp_file" "$env_file" else echo "STEM_SIGNING_ACTIVE_KEY=primary" >> "$env_file" fi @@ -53,6 +57,7 @@ tasks: desc: Start profile containers (worker + enqueuer + redis). cmds: - task: tls:certs + - task: keys:rotate - docker compose -f "{{.TASKFILE_DIR}}/docker-compose.yml" up --build compose-down: diff --git a/packages/stem/example/signing_key_rotation/Taskfile.yml b/packages/stem/example/signing_key_rotation/Taskfile.yml index a9f42b0e..6b14f7ac 100644 --- a/packages/stem/example/signing_key_rotation/Taskfile.yml +++ b/packages/stem/example/signing_key_rotation/Taskfile.yml @@ -23,7 +23,9 @@ tasks: for file in "${files[@]}"; do if grep -q "^STEM_SIGNING_KEYS=" "$file"; then - sed -i -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=${escaped_keys}|" "$file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_KEYS=.*|STEM_SIGNING_KEYS=${escaped_keys}|" "$file" > "$tmp_file" + mv "$tmp_file" "$file" else echo "STEM_SIGNING_KEYS=${keys_value}" >> "$file" fi @@ -33,7 +35,9 @@ tasks: active="rotated" fi if grep -q "^STEM_SIGNING_ACTIVE_KEY=" "$file"; then - sed -i -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=${active}|" "$file" + tmp_file="$(mktemp)" + sed -E "s|^STEM_SIGNING_ACTIVE_KEY=.*|STEM_SIGNING_ACTIVE_KEY=${active}|" "$file" > "$tmp_file" + mv "$tmp_file" "$file" else echo "STEM_SIGNING_ACTIVE_KEY=${active}" >> "$file" fi diff --git a/packages/stem/example/task_context_mixed/README.md b/packages/stem/example/task_context_mixed/README.md index 5cd70d88..f1c94323 100644 --- a/packages/stem/example/task_context_mixed/README.md +++ b/packages/stem/example/task_context_mixed/README.md @@ -70,6 +70,4 @@ task build # In separate terminals: task run-worker task run-enqueue -# Or: -task tmux ``` diff --git a/packages/stem/example/task_context_mixed/Taskfile.yml b/packages/stem/example/task_context_mixed/Taskfile.yml index 719f98dd..edb28ed9 100644 --- a/packages/stem/example/task_context_mixed/Taskfile.yml +++ b/packages/stem/example/task_context_mixed/Taskfile.yml @@ -29,15 +29,22 @@ tasks: export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" export STEM_SQLITE_BACKEND_PATH="${STEM_SQLITE_BACKEND_PATH:-task_context_mixed_backend.sqlite}" if [[ -d "$root/build/worker/bundle/bin" ]]; then - "$root/build/worker/bundle/bin/"* + shopt -s nullglob + bin_candidates=( "$root/build/worker/bundle/bin/"* ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one worker binary in $root/build/worker/bundle/bin, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" else cd "$root" dart run bin/worker.dart fi ' - run-enqueue: - desc: Run enqueue command (compiled binary when available). + _run-enqueue: + internal: true cmds: - | bash -lc ' @@ -47,48 +54,40 @@ tasks: if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" if [[ -d "$root/build/enqueue/bundle/bin" ]]; then - "$root/build/enqueue/bundle/bin/"* + shopt -s nullglob + bin_candidates=( "$root/build/enqueue/bundle/bin/"* ) + shopt -u nullglob + if (( ${#bin_candidates[@]} != 1 )); then + echo "Expected exactly one enqueue binary in $root/build/enqueue/bundle/bin, found ${#bin_candidates[@]}." >&2 + exit 1 + fi + "${bin_candidates[0]}" {{.CLI_ARGS}} else cd "$root" - dart run bin/enqueue.dart + dart run bin/enqueue.dart {{.CLI_ARGS}} fi ' + run-enqueue: + desc: Run enqueue command (compiled binary when available). + cmds: + - task: _run-enqueue + vars: + CLI_ARGS: "" + run-fail: desc: Enqueue failing payload path. cmds: - - | - bash -lc ' - set -euo pipefail - root="{{.TASKFILE_DIR}}" - env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi - export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" - if [[ -d "$root/build/enqueue/bundle/bin" ]]; then - "$root/build/enqueue/bundle/bin/"* --fail - else - cd "$root" - dart run bin/enqueue.dart --fail - fi - ' + - task: _run-enqueue + vars: + CLI_ARGS: --fail run-overwrite: desc: Enqueue overwrite path. cmds: - - | - bash -lc ' - set -euo pipefail - root="{{.TASKFILE_DIR}}" - env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi - export STEM_SQLITE_BROKER_PATH="${STEM_SQLITE_BROKER_PATH:-task_context_mixed_broker.sqlite}" - if [[ -d "$root/build/enqueue/bundle/bin" ]]; then - "$root/build/enqueue/bundle/bin/"* --overwrite - else - cd "$root" - dart run bin/enqueue.dart --overwrite - fi - ' + - task: _run-enqueue + vars: + CLI_ARGS: --overwrite run: desc: Run enqueue flow. diff --git a/packages/stem/example/unique_tasks/Taskfile.yml b/packages/stem/example/unique_tasks/Taskfile.yml index b5485494..d128fc1e 100644 --- a/packages/stem/example/unique_tasks/Taskfile.yml +++ b/packages/stem/example/unique_tasks/Taskfile.yml @@ -15,7 +15,15 @@ tasks: root="{{.TASKFILE_DIR}}" stem_root="$root/../.." env_file="${ENV_FILE:-.env}" - if [[ -f "$env_file" ]]; then set -a; source "$env_file"; set +a; elif [[ -f "$root/$env_file" ]]; then set -a; source "$root/$env_file"; set +a; fi + if [[ -f "$root/$env_file" ]]; then + set -a + source "$root/$env_file" + set +a + elif [[ -f "$env_file" ]]; then + set -a + source "$env_file" + set +a + fi cd "$stem_root" dart run example/unique_tasks/unique_task_example.dart ' diff --git a/packages/stem/lib/src/bootstrap/stem_app.dart b/packages/stem/lib/src/bootstrap/stem_app.dart index b2f511e1..63c56f6d 100644 --- a/packages/stem/lib/src/bootstrap/stem_app.dart +++ b/packages/stem/lib/src/bootstrap/stem_app.dart @@ -57,6 +57,16 @@ class StemApp { /// Registers an additional task handler with the underlying registry. void register(TaskHandler handler) => registry.register(handler); + void _insertAutoDisposers( + List Function()> autoDisposers, + ) { + if (autoDisposers.isEmpty) return; + final insertionIndex = _disposers.length >= 2 + ? _disposers.length - 2 + : _disposers.length; + _disposers.insertAll(insertionIndex, autoDisposers); + } + /// Starts the managed worker if it is not already running. Future start() async { if (_started) return; @@ -228,6 +238,7 @@ class StemApp { TaskPayloadEncoder resultEncoder = const JsonTaskPayloadEncoder(), TaskPayloadEncoder argsEncoder = const JsonTaskPayloadEncoder(), Iterable additionalEncoders = const [], + StemStack? stack, }) async { final needsUniqueLockStore = uniqueTasks && @@ -238,20 +249,22 @@ class StemApp { revokeStore == null && workerConfig.revokeStore == null; - final stack = StemStack.fromUrl( - url, - adapters: adapters, - overrides: overrides, - uniqueTasks: needsUniqueLockStore, - requireRevokeStore: needsRevokeStore, - ); + final resolvedStack = + stack ?? + StemStack.fromUrl( + url, + adapters: adapters, + overrides: overrides, + uniqueTasks: needsUniqueLockStore, + requireRevokeStore: needsRevokeStore, + ); final autoDisposers = Function()>[]; var resolvedUniqueTaskCoordinator = uniqueTaskCoordinator ?? workerConfig.uniqueTaskCoordinator; if (needsUniqueLockStore) { - final lockFactory = stack.lockStore; + final lockFactory = resolvedStack.lockStore; if (lockFactory == null) { throw StateError( 'Unique task coordination requested but lock store factory missing.', @@ -268,7 +281,7 @@ class StemApp { var resolvedRevokeStore = revokeStore ?? workerConfig.revokeStore; if (needsRevokeStore) { - final revokeFactory = stack.revokeStore; + final revokeFactory = resolvedStack.revokeStore; if (revokeFactory == null) { throw StateError('Revoke store required but no revoke factory found.'); } @@ -281,8 +294,8 @@ class StemApp { final app = await create( tasks: tasks, registry: registry, - broker: stack.broker, - backend: stack.backend, + broker: resolvedStack.broker, + backend: resolvedStack.backend, workerConfig: workerConfig, revokeStore: resolvedRevokeStore, uniqueTaskCoordinator: resolvedUniqueTaskCoordinator, @@ -296,14 +309,12 @@ class StemApp { additionalEncoders: additionalEncoders, ); - if (autoDisposers.isNotEmpty) { - // Dispose auto-provisioned lock/revoke stores after worker shutdown and - // before backend/broker factories are disposed. - app._disposers.insertAll(1, autoDisposers); - } + // Dispose auto-provisioned lock/revoke stores after worker shutdown and + // before backend/broker factories are disposed. + app._insertAutoDisposers(autoDisposers); return app; - } on Object catch (error, stack) { + } on Object catch (error, stackTrace) { // If app creation fails, release any auto-provisioned stores now to avoid // leaking startup resources. for (final disposer in autoDisposers.reversed) { @@ -313,7 +324,7 @@ class StemApp { // Keep the original startup error as the primary failure. } } - Error.throwWithStackTrace(error, stack); + Error.throwWithStackTrace(error, stackTrace); } } diff --git a/packages/stem/lib/src/bootstrap/stem_stack.dart b/packages/stem/lib/src/bootstrap/stem_stack.dart index ce91b194..0e753361 100644 --- a/packages/stem/lib/src/bootstrap/stem_stack.dart +++ b/packages/stem/lib/src/bootstrap/stem_stack.dart @@ -154,39 +154,57 @@ class StemStack { final lockUri = _resolveUri(overrides.lock, baseUri); final revokeUri = _resolveUri(overrides.revoke, backendUri); - if (scheduling && - !_hasResolvedFactory( - registered, - StemStoreKind.schedule, - scheduleUri, - (adapter) => adapter.scheduleStoreFactory(scheduleUri), - )) { + final resolvedWorkflowStore = workflows + ? _optionalFactory( + registered, + StemStoreKind.workflow, + workflowUri, + (adapter) => adapter.workflowStoreFactory(workflowUri), + ) + : null; + final resolvedScheduleStore = scheduling + ? _optionalFactory( + registered, + StemStoreKind.schedule, + scheduleUri, + (adapter) => adapter.scheduleStoreFactory(scheduleUri), + ) + : null; + final resolvedLockStore = _optionalFactory( + registered, + StemStoreKind.lock, + lockUri, + (adapter) => adapter.lockStoreFactory(lockUri), + ); + final resolvedRevokeStore = _optionalFactory( + registered, + StemStoreKind.revoke, + revokeUri, + (adapter) => adapter.revokeStoreFactory(revokeUri), + ); + + if (workflows && resolvedWorkflowStore == null) { + _failIfSqliteStoreUnsupported( + kind: StemStoreKind.workflow, + uri: workflowUri, + toggle: 'workflows', + ); + } + if (scheduling && resolvedScheduleStore == null) { _failIfSqliteStoreUnsupported( kind: StemStoreKind.schedule, uri: scheduleUri, toggle: 'scheduling', ); } - if (uniqueTasks && - !_hasResolvedFactory( - registered, - StemStoreKind.lock, - lockUri, - (adapter) => adapter.lockStoreFactory(lockUri), - )) { + if (uniqueTasks && resolvedLockStore == null) { _failIfSqliteStoreUnsupported( kind: StemStoreKind.lock, uri: lockUri, toggle: 'uniqueTasks', ); } - if (requireRevokeStore && - !_hasResolvedFactory( - registered, - StemStoreKind.revoke, - revokeUri, - (adapter) => adapter.revokeStoreFactory(revokeUri), - )) { + if (requireRevokeStore && resolvedRevokeStore == null) { _failIfSqliteStoreUnsupported( kind: StemStoreKind.revoke, uri: revokeUri, @@ -214,6 +232,7 @@ class StemStack { StemStoreKind.workflow, workflowUri, (adapter) => adapter.workflowStoreFactory(workflowUri), + preResolved: resolvedWorkflowStore, toggle: 'workflows', ) : WorkflowStoreFactory.inMemory(); @@ -224,6 +243,7 @@ class StemStack { StemStoreKind.schedule, scheduleUri, (adapter) => adapter.scheduleStoreFactory(scheduleUri), + preResolved: resolvedScheduleStore, toggle: 'scheduling', ) : null; @@ -234,14 +254,10 @@ class StemStack { StemStoreKind.lock, lockUri, (adapter) => adapter.lockStoreFactory(lockUri), + preResolved: resolvedLockStore, toggle: 'uniqueTasks', ) - : _optionalFactory( - registered, - StemStoreKind.lock, - lockUri, - (adapter) => adapter.lockStoreFactory(lockUri), - ); + : resolvedLockStore; final revokeStore = requireRevokeStore ? _requireFactory( @@ -249,14 +265,10 @@ class StemStack { StemStoreKind.revoke, revokeUri, (adapter) => adapter.revokeStoreFactory(revokeUri), + preResolved: resolvedRevokeStore, toggle: 'requireRevokeStore', ) - : _optionalFactory( - registered, - StemStoreKind.revoke, - revokeUri, - (adapter) => adapter.revokeStoreFactory(revokeUri), - ); + : resolvedRevokeStore; return StemStack._( broker: broker, @@ -331,7 +343,9 @@ T _requireFactory( Uri uri, T? Function(StemStoreAdapter adapter) resolver, { String? toggle, + T? preResolved, }) { + if (preResolved != null) return preResolved; final matched = []; for (final adapter in adapters) { if (!adapter.supports(uri, kind)) continue; @@ -369,15 +383,6 @@ T? _optionalFactory( return null; } -bool _hasResolvedFactory( - Iterable adapters, - StemStoreKind kind, - Uri uri, - T? Function(StemStoreAdapter adapter) resolver, -) { - return _optionalFactory(adapters, kind, uri, resolver) != null; -} - void _failIfSqliteStoreUnsupported({ required StemStoreKind kind, required Uri uri, diff --git a/packages/stem/lib/src/bootstrap/workflow_app.dart b/packages/stem/lib/src/bootstrap/workflow_app.dart index c5701185..06a10f68 100644 --- a/packages/stem/lib/src/bootstrap/workflow_app.dart +++ b/packages/stem/lib/src/bootstrap/workflow_app.dart @@ -374,6 +374,7 @@ class StemWorkflowApp { url, adapters: adapters, overrides: overrides, + stack: stack, workerConfig: workerConfig, uniqueTasks: uniqueTasks, uniqueTaskDefaultTtl: uniqueTaskDefaultTtl, @@ -400,12 +401,8 @@ class StemWorkflowApp { leaseExtension: leaseExtension, workflowRegistry: workflowRegistry, introspectionSink: introspectionSink, - encoderRegistry: encoderRegistry, - resultEncoder: resultEncoder, - argsEncoder: argsEncoder, - additionalEncoders: additionalEncoders, ); - } on Object catch (error, stack) { + } on Object catch (error, stackTrace) { // fromUrl owns the app instance; clean it up when workflow bootstrap // fails. try { @@ -413,7 +410,7 @@ class StemWorkflowApp { } on Object { // Keep the original bootstrap failure as the primary error. } - Error.throwWithStackTrace(error, stack); + Error.throwWithStackTrace(error, stackTrace); } } diff --git a/packages/stem/test/bootstrap/stem_app_test.dart b/packages/stem/test/bootstrap/stem_app_test.dart index a60319cd..dda378f1 100644 --- a/packages/stem/test/bootstrap/stem_app_test.dart +++ b/packages/stem/test/bootstrap/stem_app_test.dart @@ -1,6 +1,8 @@ import 'package:stem/stem.dart'; import 'package:test/test.dart'; +import 'test_store_adapter.dart'; + void main() { group('StemApp', () { test('inMemory executes tasks', () async { @@ -125,8 +127,9 @@ void main() { name: 'test.from-url', entrypoint: (context, args) async => null, ); - final adapter = _BootstrapAdapter( + final adapter = TestStoreAdapter( scheme: 'test', + adapterName: 'bootstrap-test-adapter', broker: StemBrokerFactory(create: () async => InMemoryBroker()), backend: StemBackendFactory( create: () async => InMemoryResultBackend(), @@ -158,8 +161,9 @@ void main() { final createdRevokeStore = InMemoryRevokeStore(); var lockDisposed = false; var revokeDisposed = false; - final adapter = _BootstrapAdapter( + final adapter = TestStoreAdapter( scheme: 'test', + adapterName: 'bootstrap-test-adapter', broker: StemBrokerFactory(create: () async => InMemoryBroker()), backend: StemBackendFactory( create: () async => InMemoryResultBackend(), @@ -195,47 +199,49 @@ void main() { test( 'fromUrl disposes auto-wired stores when app bootstrap fails', () async { - final createdLockStore = InMemoryLockStore(); - final createdRevokeStore = InMemoryRevokeStore(); - var lockDisposed = false; - var revokeDisposed = false; - final adapter = _BootstrapAdapter( - scheme: 'test', - broker: StemBrokerFactory( - create: () async => throw StateError('broker bootstrap failure'), - ), - backend: StemBackendFactory( - create: () async => InMemoryResultBackend(), - ), - lock: LockStoreFactory( - create: () async => createdLockStore, - dispose: (store) async => lockDisposed = true, - ), - revoke: RevokeStoreFactory( - create: () async => createdRevokeStore, - dispose: (store) async => revokeDisposed = true, - ), - ); + final createdLockStore = InMemoryLockStore(); + final createdRevokeStore = InMemoryRevokeStore(); + var lockDisposed = false; + var revokeDisposed = false; + final adapter = TestStoreAdapter( + scheme: 'test', + adapterName: 'bootstrap-test-adapter', + broker: StemBrokerFactory( + create: () async => throw StateError('broker bootstrap failure'), + ), + backend: StemBackendFactory( + create: () async => InMemoryResultBackend(), + ), + lock: LockStoreFactory( + create: () async => createdLockStore, + dispose: (store) async => lockDisposed = true, + ), + revoke: RevokeStoreFactory( + create: () async => createdRevokeStore, + dispose: (store) async => revokeDisposed = true, + ), + ); - await expectLater( - () => StemApp.fromUrl( - 'test://localhost', - adapters: [adapter], - uniqueTasks: true, - requireRevokeStore: true, - ), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('broker bootstrap failure'), + await expectLater( + () => StemApp.fromUrl( + 'test://localhost', + adapters: [adapter], + uniqueTasks: true, + requireRevokeStore: true, ), - ), - ); + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('broker bootstrap failure'), + ), + ), + ); - expect(lockDisposed, isTrue); - expect(revokeDisposed, isTrue); - }); + expect(lockDisposed, isTrue); + expect(revokeDisposed, isTrue); + }, + ); }); group('StemWorkflowApp', () { @@ -364,8 +370,9 @@ void main() { builder.step('hello', (ctx) async => 'from-url'); }, ); - final adapter = _BootstrapAdapter( + final adapter = TestStoreAdapter( scheme: 'test', + adapterName: 'bootstrap-test-adapter', broker: StemBrokerFactory(create: () async => InMemoryBroker()), backend: StemBackendFactory( create: () async => InMemoryResultBackend(), @@ -397,8 +404,9 @@ void main() { final createdRevokeStore = InMemoryRevokeStore(); var lockDisposed = false; var revokeDisposed = false; - final adapter = _BootstrapAdapter( + final adapter = TestStoreAdapter( scheme: 'test', + adapterName: 'bootstrap-test-adapter', broker: StemBrokerFactory(create: () async => InMemoryBroker()), backend: StemBackendFactory( create: () async => InMemoryResultBackend(), @@ -489,45 +497,3 @@ class _TestMiddleware implements Middleware { Future onExecute(TaskContext context, Future Function() next) => next(); } - -class _BootstrapAdapter implements StemStoreAdapter { - _BootstrapAdapter({ - required this.scheme, - this.broker, - this.backend, - this.workflow, - this.lock, - this.revoke, - }); - - final String scheme; - final StemBrokerFactory? broker; - final StemBackendFactory? backend; - final WorkflowStoreFactory? workflow; - final LockStoreFactory? lock; - final RevokeStoreFactory? revoke; - - @override - String get name => 'bootstrap-test-adapter'; - - @override - bool supports(Uri uri, StemStoreKind kind) => uri.scheme == scheme; - - @override - StemBrokerFactory? brokerFactory(Uri uri) => broker; - - @override - StemBackendFactory? backendFactory(Uri uri) => backend; - - @override - WorkflowStoreFactory? workflowStoreFactory(Uri uri) => workflow; - - @override - ScheduleStoreFactory? scheduleStoreFactory(Uri uri) => null; - - @override - LockStoreFactory? lockStoreFactory(Uri uri) => lock; - - @override - RevokeStoreFactory? revokeStoreFactory(Uri uri) => revoke; -} diff --git a/packages/stem/test/bootstrap/stem_client_test.dart b/packages/stem/test/bootstrap/stem_client_test.dart index 069ceeac..9443252e 100644 --- a/packages/stem/test/bootstrap/stem_client_test.dart +++ b/packages/stem/test/bootstrap/stem_client_test.dart @@ -1,6 +1,8 @@ import 'package:stem/stem.dart'; import 'package:test/test.dart'; +import 'test_store_adapter.dart'; + void main() { test('StemClient inMemory runs workflow end-to-end', () async { final client = await StemClient.inMemory(); @@ -34,8 +36,9 @@ void main() { final client = await StemClient.fromUrl( 'test://localhost', adapters: [ - _ClientAdapter( + TestStoreAdapter( scheme: 'test', + adapterName: 'client-test-adapter', broker: StemBrokerFactory(create: () async => InMemoryBroker()), backend: StemBackendFactory( create: () async => InMemoryResultBackend(), @@ -60,39 +63,3 @@ void main() { } }); } - -class _ClientAdapter implements StemStoreAdapter { - _ClientAdapter({ - required this.scheme, - this.broker, - this.backend, - }); - - final String scheme; - final StemBrokerFactory? broker; - final StemBackendFactory? backend; - - @override - String get name => 'client-test-adapter'; - - @override - bool supports(Uri uri, StemStoreKind kind) => uri.scheme == scheme; - - @override - StemBrokerFactory? brokerFactory(Uri uri) => broker; - - @override - StemBackendFactory? backendFactory(Uri uri) => backend; - - @override - WorkflowStoreFactory? workflowStoreFactory(Uri uri) => null; - - @override - ScheduleStoreFactory? scheduleStoreFactory(Uri uri) => null; - - @override - LockStoreFactory? lockStoreFactory(Uri uri) => null; - - @override - RevokeStoreFactory? revokeStoreFactory(Uri uri) => null; -} diff --git a/packages/stem/test/bootstrap/stem_stack_test.dart b/packages/stem/test/bootstrap/stem_stack_test.dart index a4691f65..54fd37a6 100644 --- a/packages/stem/test/bootstrap/stem_stack_test.dart +++ b/packages/stem/test/bootstrap/stem_stack_test.dart @@ -237,6 +237,22 @@ void main() { ); }); + test('fails fast for sqlite workflow store requests', () { + expect( + () => StemStack.fromUrl('sqlite:///tmp/stem.db', workflows: true), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('do not provide a workflow store'), + contains('Disable `workflows`'), + ), + ), + ), + ); + }); + test('fails fast for sqlite lock store requests', () { expect( () => StemStack.fromUrl('sqlite:///tmp/stem.db', uniqueTasks: true), @@ -275,43 +291,43 @@ void main() { test( 'accepts sqlite stores when custom adapters provide toggle factories', () { - final brokerFactory = StemBrokerFactory( - create: () async => InMemoryBroker(), - ); - final backendFactory = StemBackendFactory( - create: () async => InMemoryResultBackend(), - ); - final scheduleFactory = ScheduleStoreFactory( - create: () async => InMemoryScheduleStore(), - ); - final lockFactory = LockStoreFactory( - create: () async => InMemoryLockStore(), - ); - final revokeFactory = RevokeStoreFactory( - create: () async => InMemoryRevokeStore(), - ); - - final adapter = _TestAdapter( - scheme: 'sqlite', - brokerFactory: brokerFactory, - backendFactory: backendFactory, - scheduleStoreFactory: scheduleFactory, - lockStoreFactory: lockFactory, - revokeStoreFactory: revokeFactory, - ); - - final stack = StemStack.fromUrl( - 'sqlite:///tmp/stem.db', - adapters: [adapter], - scheduling: true, - uniqueTasks: true, - requireRevokeStore: true, - ); + final brokerFactory = StemBrokerFactory( + create: () async => InMemoryBroker(), + ); + final backendFactory = StemBackendFactory( + create: () async => InMemoryResultBackend(), + ); + final scheduleFactory = ScheduleStoreFactory( + create: () async => InMemoryScheduleStore(), + ); + final lockFactory = LockStoreFactory( + create: () async => InMemoryLockStore(), + ); + final revokeFactory = RevokeStoreFactory( + create: () async => InMemoryRevokeStore(), + ); + + final adapter = _TestAdapter( + scheme: 'sqlite', + brokerFactory: brokerFactory, + backendFactory: backendFactory, + scheduleStoreFactory: scheduleFactory, + lockStoreFactory: lockFactory, + revokeStoreFactory: revokeFactory, + ); + + final stack = StemStack.fromUrl( + 'sqlite:///tmp/stem.db', + adapters: [adapter], + scheduling: true, + uniqueTasks: true, + requireRevokeStore: true, + ); - expect(stack.broker, same(brokerFactory)); - expect(stack.backend, same(backendFactory)); - expect(stack.scheduleStore, same(scheduleFactory)); - expect(stack.lockStore, same(lockFactory)); + expect(stack.broker, same(brokerFactory)); + expect(stack.backend, same(backendFactory)); + expect(stack.scheduleStore, same(scheduleFactory)); + expect(stack.lockStore, same(lockFactory)); expect(stack.revokeStore, same(revokeFactory)); }, ); diff --git a/packages/stem/test/bootstrap/test_store_adapter.dart b/packages/stem/test/bootstrap/test_store_adapter.dart new file mode 100644 index 00000000..de6ff0d9 --- /dev/null +++ b/packages/stem/test/bootstrap/test_store_adapter.dart @@ -0,0 +1,47 @@ +import 'package:stem/stem.dart'; + +class TestStoreAdapter implements StemStoreAdapter { + const TestStoreAdapter({ + required this.scheme, + this.adapterName = 'test-store-adapter', + this.broker, + this.backend, + this.workflow, + this.schedule, + this.lock, + this.revoke, + }); + + final String scheme; + final String adapterName; + final StemBrokerFactory? broker; + final StemBackendFactory? backend; + final WorkflowStoreFactory? workflow; + final ScheduleStoreFactory? schedule; + final LockStoreFactory? lock; + final RevokeStoreFactory? revoke; + + @override + String get name => adapterName; + + @override + bool supports(Uri uri, StemStoreKind kind) => uri.scheme == scheme; + + @override + StemBrokerFactory? brokerFactory(Uri uri) => broker; + + @override + StemBackendFactory? backendFactory(Uri uri) => backend; + + @override + WorkflowStoreFactory? workflowStoreFactory(Uri uri) => workflow; + + @override + ScheduleStoreFactory? scheduleStoreFactory(Uri uri) => schedule; + + @override + LockStoreFactory? lockStoreFactory(Uri uri) => lock; + + @override + RevokeStoreFactory? revokeStoreFactory(Uri uri) => revoke; +} diff --git a/packages/stem_adapter_tests/CHANGELOG.md b/packages/stem_adapter_tests/CHANGELOG.md index 14c133be..48cb9d8f 100644 --- a/packages/stem_adapter_tests/CHANGELOG.md +++ b/packages/stem_adapter_tests/CHANGELOG.md @@ -1,4 +1,6 @@ -## Unreleased +# Changelog + +## 0.1.1 - Expanded adapter contract documentation with a capability matrix, explicit skip semantics, and recipe-style setup examples. diff --git a/packages/stem_builder/CHANGELOG.md b/packages/stem_builder/CHANGELOG.md index 2f143aed..e7dcb657 100644 --- a/packages/stem_builder/CHANGELOG.md +++ b/packages/stem_builder/CHANGELOG.md @@ -1,4 +1,6 @@ -## Unreleased +# Changelog + +## 0.1.0 - Initial builder for annotated Stem workflow/task registries. - Expanded the registry builder implementation and hardened generation output. diff --git a/packages/stem_cli/CHANGELOG.md b/packages/stem_cli/CHANGELOG.md index 89377d49..c858777a 100644 --- a/packages/stem_cli/CHANGELOG.md +++ b/packages/stem_cli/CHANGELOG.md @@ -1,6 +1,4 @@ -## Unreleased - -- No package-specific runtime changes in this workspace update. +# Changelog ## 0.1.0 diff --git a/packages/stem_memory/CHANGELOG.md b/packages/stem_memory/CHANGELOG.md index 308e5dbb..208e435a 100644 --- a/packages/stem_memory/CHANGELOG.md +++ b/packages/stem_memory/CHANGELOG.md @@ -1,4 +1,6 @@ -## Unreleased +# Changelog + +## 0.1.0 - Renamed `memoryBackendFactory` to `memoryResultBackendFactory` for adapter factory naming consistency. diff --git a/packages/stem_postgres/CHANGELOG.md b/packages/stem_postgres/CHANGELOG.md index 09fe12ad..c7c3539c 100644 --- a/packages/stem_postgres/CHANGELOG.md +++ b/packages/stem_postgres/CHANGELOG.md @@ -1,11 +1,10 @@ -## Unreleased +# Changelog + +## 0.1.0 - Normalized `postgresResultBackendFactory` to accept a positional `uri` argument, matching the adapter factory style used across packages. - Updated Postgres adapter wiring to use the new factory signature. - -## 0.1.0 - - Added workflow run lease tracking and claim/renew helpers to distribute workflow execution safely across workers. - Fixed worker heartbeat lookups by restoring soft-deleted heartbeat rows on diff --git a/packages/stem_redis/CHANGELOG.md b/packages/stem_redis/CHANGELOG.md index 80f55210..48f65074 100644 --- a/packages/stem_redis/CHANGELOG.md +++ b/packages/stem_redis/CHANGELOG.md @@ -1,6 +1,6 @@ - +# Changelog -## Unreleased +## 0.1.1 - Enabled broadcast fan-out broker contract coverage in Redis integration tests by wiring additional broker instances for shared-namespace fan-out checks. diff --git a/packages/stem_sqlite/CHANGELOG.md b/packages/stem_sqlite/CHANGELOG.md index f5af342b..238fa2a4 100644 --- a/packages/stem_sqlite/CHANGELOG.md +++ b/packages/stem_sqlite/CHANGELOG.md @@ -1,4 +1,6 @@ -## Unreleased +# Changelog + +## 0.1.1 - Added broker broadcast fan-out support for SQLite routing subscriptions with broadcast channels. From 0491f1c3dedc0e492d5ccfd8aa51a2ec752bcbc6 Mon Sep 17 00:00:00 2001 From: kingwill101 Date: Tue, 24 Feb 2026 08:52:37 -0500 Subject: [PATCH 4/4] test(stem_cli): cover cloud config helpers --- .../test/unit/cli/cloud_config_test.dart | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/stem_cli/test/unit/cli/cloud_config_test.dart diff --git a/packages/stem_cli/test/unit/cli/cloud_config_test.dart b/packages/stem_cli/test/unit/cli/cloud_config_test.dart new file mode 100644 index 00000000..d69e8782 --- /dev/null +++ b/packages/stem_cli/test/unit/cli/cloud_config_test.dart @@ -0,0 +1,54 @@ +import 'package:stem_cli/src/cli/cloud_config.dart'; +import 'package:test/test.dart'; + +void main() { + group('cloud config helpers', () { + test('resolveStemCloudAuthToken prefers non-empty override', () { + final token = resolveStemCloudAuthToken({ + kStemCloudAccessTokenEnv: 'env-token', + kStemCloudApiKeyEnv: 'api-key', + }, override: ' override-token '); + + expect(token, 'override-token'); + }); + + test('resolveStemCloudApiKey falls back to API key env var', () { + final token = resolveStemCloudApiKey({ + kStemCloudApiKeyEnv: 'api-key-value', + }); + + expect(token, 'api-key-value'); + }); + + test('resolveStemCloudApiKey throws when no token sources are set', () { + expect( + () => resolveStemCloudApiKey(const {}), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains(kStemCloudAccessTokenEnv), + ), + ), + ); + }); + + test('resolveStemCloudNamespace uses configured fallback order', () { + expect( + resolveStemCloudNamespace({ + kStemNamespaceEnv: 'default-ns', + kStemWorkerNamespaceEnv: 'worker-ns', + }), + 'default-ns', + ); + + expect( + resolveStemCloudNamespace({ + kStemCloudNamespaceEnv: ' cloud-ns ', + kStemNamespaceEnv: 'default-ns', + }), + 'cloud-ns', + ); + }); + }); +}