feat(DT-4385): add a CLI to demo an in-progress feature - #3842
Open
rossnelson wants to merge 7 commits into
Open
feat(DT-4385): add a CLI to demo an in-progress feature#3842rossnelson wants to merge 7 commits into
rossnelson wants to merge 7 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
rossnelson
force-pushed
the
dt-4385-feature-demo-cli
branch
from
August 20, 2026 13:29
dc66550 to
629c3d5
Compare
rossnelson
force-pushed
the
dt-4385-feature-demo-cli
branch
from
August 20, 2026 13:37
629c3d5 to
5f0c363
Compare
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.
rossnelson
force-pushed
the
dt-4385-feature-demo-cli
branch
from
August 20, 2026 13:49
5f0c363 to
97b8fd6
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Based on
decode-binary-protobuf-payloads, because the demo it ships is a demoof 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:
The command runs the stages, prints the summary, and exits. The run continues.
pnpm demo stopends it, and--onceends 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 Catalogexample is, and its files are TypeScript:
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:
A scenario says what to run in one of two ways, and it can use both.
examplesnames Catalog examples. A
scenario.tsgives behaviour that names of examplescannot give.
Stages
Four stages run in sequence, and each one is optional. Use
"enabled": falseinthe definition, or
--skipand--only.serverworkeruiscenariosA 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:
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:
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
replacedirective and it writes nogo.workin a checkout. Adefinition gives no path to a checkout, because a path belongs to a machine. Set
TEMPORAL_SERVER_REPOandTEMPORAL_CLI_REPO, or copy.env.feature-demo.local.example. The paths are read only if a build isnecessary.
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 forthe payloads of the operation, which are
binary/protobufworkflowservicemessages, thus
payload-converter.tssupplies one and the caller runs on aworker 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 listshows the Catalog examples and their ids, which a personneeds 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
pnpm demo list, thenpnpm demo start system-nexus-signal-with-start.adds.
pnpm demo stop.pnpm lint:ci,pnpm check,pnpm catalog verify, andpnpm test -- --runallpass. 44 tests cover the dispatcher, the selection of a server, and the reasons
this scenario ships a caller of its own.