Skip to content

feat(DT-4385): add a CLI to demo an in-progress feature - #3842

Open
rossnelson wants to merge 7 commits into
decode-binary-protobuf-payloadsfrom
dt-4385-feature-demo-cli
Open

feat(DT-4385): add a CLI to demo an in-progress feature#3842
rossnelson wants to merge 7 commits into
decode-binary-protobuf-payloadsfrom
dt-4385-feature-demo-cli

Conversation

@rossnelson

@rossnelson rossnelson commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Based on decode-binary-protobuf-payloads, because the demo it ships is a demo
of that branch. Review this after DT-4385, or with it.

The goal

A reviewer of an in-progress feature needs a Temporal server that has the
feature, the dynamic configuration it wants, a worker, the workflows that show
it, and a list of what to check. One command supplies all of it:

pnpm demo start system-nexus-signal-with-start

The command runs the stages, prints the summary, and exits. The run continues.
pnpm demo stop ends it, and --once ends it before the command returns.

This tool is for in-progress features. The system Nexus SignalWithStart
operation is in server 1.32.0, and the newest CLI release has server 1.31.2, thus
a release cannot show it and the tool compiles a server checkout instead.

How a scenario is put together

Everything is in utilities/demo. A scenario is a directory, the way a Catalog
example is, and its files are TypeScript:

utilities/demo/scenarios/system-nexus-signal-with-start/
  definition.ts         what to run and what to check
  scenario.ts           behaviour that names of examples cannot give
  caller-workflow.ts    the workflow that sends the operation
  payload-converter.ts  binary/protobuf for the two messages

A definition is a module, not JSON, because the shape has a meaning at runtime
only and the compiler checks a module at no cost. Therefore the options that a
definition sends to its scenario are checked, and the editor shows them:

error TS2561: Object literal may only specify known properties, but
'signalNaem' does not exist in type '{ ... signalName?: string ... }'.
Did you mean to write 'signalName'?

A scenario says what to run in one of two ways, and it can use both. examples
names Catalog examples. A scenario.ts gives behaviour that names of examples
cannot give.

Stages

Four stages run in sequence, and each one is optional. Use "enabled": false in
the definition, or --skip and --only.

Stage What it does
server Supplies a Temporal dev server and applies the dynamic config
worker Starts the Catalog worker against that server
ui Starts the ui-server API and the UI dev server
scenarios Runs the workflows that show the feature

A start stops a recorded run of the same scenario first, and it says so. Anything
on a port that no record mentions belongs to somebody else, and a stage reuses
it. A stop kills the recorded pids and then sweeps the ports that the run owns.

The server stage

The stage selects the least costly server that has the feature. A definition
gives the commit that added it:

"requires": { "serverCommit": "01aa279c462fd9e7efc8e0ba6bbc4554b51557dd" }

The stage finds the release line of that commit from its tags, then it tries a
CLI on disk, then a release, then a build of your server checkout. It gives the
reason:

Commit 01aa279c4 is on main and first tagged 1.32.0-157.0, so any Server
1.32.0-157.0 or later has it.
The latest CLI release bundles Server 1.31.2, which is too old. Compiling the
server checkout instead.

When a release has the operation, the same definition uses that release, with no
change and no Go tools.

The build uses a Go workspace in .feature-demo, thus it changes no repository:
it adds no replace directive and it writes no go.work in a checkout. A
definition gives no path to a checkout, because a path belongs to a machine. Set
TEMPORAL_SERVER_REPO and TEMPORAL_CLI_REPO, or copy
.env.feature-demo.local.example. The paths are read only if a build is
necessary.

The Catalog does the work

The workflows are Catalog examples and the worker is the Catalog worker, thus a
demo shows the same code that the Catalog page runs. A definition gives an
example id only, and the workflow type, the task queue, and the input schema come
from the Catalog. An input that disagrees with the schema of an example stops the
run before anything starts.

The caller of the operation is an ordinary workflow, because the reserved
__temporal_ prefix is refused by the Go SDK only. TypeScript has no encoder for
the payloads of the operation, which are binary/protobuf workflowservice
messages, thus payload-converter.ts supplies one and the caller runs on a
worker of its own. That converter is scaffolding: two tests fail when the SDK
makes it unnecessary, and each one says what to delete. The scenario also reads
the request that the server recorded and stops the run if a field that the UI
needs is absent.

Also here

pnpm catalog list shows the Catalog examples and their ids, which a person
needs to write a definition. It is a separate commit, and it can become a
separate PR if the Catalog owners prefer.

The system Nexus skill documents that the Go SDK alone refuses the reserved
prefix, and that the true limit for TypeScript is the encoding of the payloads.

To review

  1. pnpm demo list, then pnpm demo start system-nexus-signal-with-start.
  2. Follow the steps that the summary gives. They check the display that DT-4385
    adds.
  3. pnpm demo stop.

pnpm lint:ci, pnpm check, pnpm catalog verify, and pnpm test -- --run all
pass. 44 tests cover the dispatcher, the selection of a server, and the reasons
this scenario ships a caller of its own.

@rossnelson
rossnelson requested a review from a team as a code owner August 19, 2026 23:07
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview Aug 20, 2026 4:36pm

Request Review

One directory gives a reviewer everything necessary to see a feature
operate: the Temporal server to run, the dynamic configuration to enable
on it, the workflows to run against it, and the steps to check.

    pnpm demo start system-nexus-signal-with-start

A scenario is a directory, the way a Catalog example is, and its files
are TypeScript:

    utilities/demo/scenarios/system-nexus-signal-with-start/
      definition.ts        what to run and what to check
      scenario.ts          behaviour that names of examples cannot give
      caller-workflow.ts   the workflow that sends the operation
      payload-converter.ts binary/protobuf for the two messages

A name has a prefix for its group, thus the scenarios of one endpoint
stay together in "pnpm demo list".

The definition is a module, not JSON, because the shape has a meaning at
runtime only and the compiler checks a module at no cost. Therefore the
options that a definition sends to its scenario are checked, and the
editor shows them:

    error TS2561: Object literal may only specify known properties, but
    'signalNaem' does not exist in type '{ ... signalName?: string ... }'.
    Did you mean to write 'signalName'?

A scenario says what to run in one of two ways, and it can use both.
"examples" names Catalog examples. A scenario.ts gives behaviour that
names of examples cannot give.

Four stages run in sequence, and each one is optional. Use "enabled":
false in the definition, or --skip and --only. A stage also stops by
itself if a port is in use, thus a server or a UI that you started is
used again.

  server     Supplies a Temporal dev server and applies the dynamic
             configuration.
  worker     Starts the Catalog worker against that server.
  ui         Starts the ui-server API and the UI dev server.
  scenarios  Runs the workflows that show the feature.

The server stage selects the least costly server that has the feature. A
definition gives requires.serverCommit, and the stage finds the release
line of that commit from its tags. A CLI on disk costs nothing, a
release costs a download, and a build of a local server checkout costs
minutes, thus it tries them in that sequence.

A definition gives no path to a checkout, because a path belongs to a
machine. Set TEMPORAL_SERVER_REPO and TEMPORAL_CLI_REPO, or put them in
.env.feature-demo.local. See .env.feature-demo.local.example. The paths
are read only if a build is necessary.

The workflows are Catalog examples and the worker is the Catalog worker,
thus a demo shows the same code that the Catalog page runs. A definition
gives an example id only. The workflow type, the task queue, and the
input schema come from the Catalog.
The Catalog knew what examples it has, but no command showed them. A
person who writes a scenario definition had to read
catalog.generated.json to find an example id.

    pnpm catalog list          # id, source, target, workflow type
    pnpm catalog list --json   # the same, for a script

The command reads the examples from the adapter, thus it gives the
Catalog's own answer and it does not read the generated artifacts.
The skill said that no SDK can send this operation from a workflow. That
is correct for the Go SDK only. The TypeScript SDK has no check on the
reserved __temporal_ prefix, and the demo scenario now sends the
operation from a usual caller workflow.

The true limit is the payloads: they are binary/protobuf workflowservice
messages, and the SDK has no encoder for them. Therefore the scenario
supplies one and it runs the caller on a worker of its own, thus the
Catalog worker gets no change. The Python SDK has the operation as
workflow.signal_with_start_workflow() on a branch.

The skill also gives the difference between the Nexus operation and the
signalWithStart function of the client, because only the operation makes
the events that this branch shows.
The UI shows signalName, identity, and control from the decoded request.
The server records the request as the caller sent it and adds nothing,
and no SDK sets identity on this path, thus the identity row never
appeared and a review step asked for something that could not happen.

The caller now names itself. The preview also says that a control row
appears for a caller that sets that field only.
This scenario is joined to two things that will change, and both changes
are quiet: the shape of the recorded request, and the arrival of support
in the TypeScript SDK. Nothing failed when the shape lost a field, thus
the identity row was absent for some time and nobody saw it.

The scenario now reads the request that the server recorded and it stops
the run if a field that the UI needs is absent:

    FAILED  The recorded request no longer carries: identity.

It also reports a field that appears but that this caller does not send,
because the UI then shows more than the review steps say.

Two tests fail when the SDK gets support: one for a signal-with-start
API on @temporalio/workflow, and one for a converter that can put these
messages in binary/protobuf. Each one says what to delete.

Both mechanisms were confirmed by a break of the thing they watch.

Scenarios no longer give a ticket id, because a ticket id has a meaning
inside one team only.
The processes that a run starts are detached and in their own process
groups, thus they outlive the command already. The command held a
terminal to own something it did not own, and the hold cost a keep-alive
handle, two signal handlers, an escape for a second Ctrl-C, a
supervisorPid in the state, and two kinds of orphan: a supervisor that
died and left its children, then children that outlived their record.

Now "demo start" runs the stages, prints the summary, records what it
owns, and exits. "demo stop" ends the run. "--once" tears the run down
before it returns, which is what one check wants, and it replaces
--no-keep-alive.

A start also stops a recorded run of the same scenario first, and it says
so, because a start means a person wants a fresh run. Something on a port
that no record mentions belongs to somebody else and the stages reuse it,
as before.

A stop now sweeps the ports that the run owns after it kills the recorded
pids, thus a child that outlived its record still ends. A child with no
port, such as the Catalog worker, needs its recorded pid: a sweep by
process name would also end a worker that a person started.
A build of the dev server fetches temporalio/temporal and temporalio/cli
at the refs a scenario names. A blobless shallow fetch of one commit
costs about a second, thus the demo needs no configuration, it does not
compile whatever a person has open, and it cannot take a replace
directive from another tree. TEMPORAL_SERVER_REPO and TEMPORAL_CLI_REPO
still win, because a person who develops the feature wants their own
working tree.

The two refs are a pair. The workspace puts both modules in one
dependency graph, thus Go resolves go.temporal.io/api to the highest
requirement across the two and both trees compile against that one
version. The repositories are not released in step, thus an arbitrary
pair fails, and the current main of each is such a pair: the CLI names
v1.63.4, the server names v1.63.5, and each removes something that the
other uses. This scenario names two commits that agree on v1.63.0.

requires.serverCommit is a floor, not a target for a build. A definition
states minServerVersion beside it, because a fetched checkout has one
commit and cannot give the release line of a commit. A checkout with
history is still checked to contain the commit.

A failed build now reports the compile output and the pair that produced
it. Before, it reported an empty message.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant