Skip to content

fix(fs/embed): harden runtime pack update lifecycle#354

Merged
wolfy-j merged 8 commits into
mainfrom
fix/fs-embed-lifecycle-hardening
Jun 17, 2026
Merged

fix(fs/embed): harden runtime pack update lifecycle#354
wolfy-j merged 8 commits into
mainfrom
fix/fs-embed-lifecycle-hardening

Conversation

@wolfy-j

@wolfy-j wolfy-j commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Follow-up hardening for the fs/embed live install/update path after #353.

What changed:

  • Clean up partially prepared embedded packs if Prepare fails before the planner can own rollback.
  • Restage same-version packs when the runtime registry is missing them instead of assuming installed metadata means the pack is live.
  • Drop stale embedded packs when a resolved module now comes from a directory or replacement.
  • Honor lock unpack_modules in live dependency resolution, including cached/downloaded .wapp extraction.
  • Extract unpacked modules into a temp directory and swap after success so failed extraction preserves the currently serving directory.
  • Register module source roots for runtime-loaded directory modules so fs.directory entries with base: module resolve after AppContext seal.
  • Emit delete+create for same-ID kind changes and allow directive expansion to carry that replacement pair.
  • Share the .wapp-to-directory extraction logic with boot/deps so runtime code does not import cmd/internal.

Verification:

  • go test -race -short -tags "fts5 sqlite_vec treesitter" ./boot/deps/hub ./boot/deps/wappextract ./cmd/internal/entries ./service/fs/embed ./api/service/fs/embed ./system/registry/expansion
  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0 run --timeout=10m --build-tags=race ./boot/deps/hub ./boot/deps/wappextract ./cmd/internal/entries ./service/fs/embed ./api/service/fs/embed ./system/registry/expansion
  • make test
  • make lint

skhaz and others added 8 commits June 16, 2026 19:21
Why:
Spawning a workflow whose registry ID has no namespace (e.g. an external,
PHP-hosted workflow) recorded the Temporal WorkflowType as
":PopulateItemRelation" instead of "PopulateItemRelation". registry.ID.String()
renders a namespace-less ID in its canonical ":name" form, and the Temporal
layer used that string directly as the external workflow/activity type name.

What:
Add registry.ID.Qualified(), which returns "name" when the namespace is empty
and "ns:name" otherwise (never a leading colon), mirroring MarshalJSON. Switch
every Temporal type-name derivation -- both dispatch (start) and registration --
from String() to Qualified(), so namespaced names are unchanged while
namespace-less names lose the spurious colon symmetrically on both sides.
…name option

Why:
Temporal workflow/activity type names are plain strings that only sometimes
coincide with a registry ID. Deriving them by stringifying registry.ID ad-hoc
across the dispatch paths coupled the boundary to ID semantics (the source of
the leading-colon bug) and offered no way to name an external workflow whose
type string is not a registry entry.

What:
Add a single boundary-name resolver (service/temporal/options/name.go):
WorkflowName/ActivityName prefer an explicit "workflow.name"/"activity.name"
option (with temporal.* legacy alias) and fall back to id.Qualified();
WorkflowNameFromID/ActivityNameFromID cover sites with no options bag. Route
every dispatch site (host spawn, child spawn/exec, workflow exec, activity
dispatch, continue-as-new) through the resolver, demoting registry.ID to a
naming fallback while keeping it as the local-path key on Start.Source. Add the
symmetric MetaActivityName registration override mirroring MetaWorkflowName, so
an activity definition can declare a string name; default stays Qualified().
Why:
The colon-free type-name derivation was added as a registry.ID.Qualified()
method, putting Temporal-boundary concerns on the generic registry value type.

What:
Remove registry.ID.Qualified() (revert api/registry/id.go and id_test.go to
their original form) and move the logic into an unexported qualify() in
service/temporal/options/name.go, used by the WorkflowName/ActivityName
resolvers. The workflow and activity listeners now derive their fallback name
via temporaloptions.WorkflowNameFromID/ActivityNameFromID. Behaviour is
identical (bare name when the namespace is empty, "ns:name" otherwise).
Why:
Five temporal app-test fixtures passed a Message? value where payload_data
expects a non-optional Message, a latent Luau type error. It never surfaced
because CI always sets SKIP_TEMPORAL_TESTS, but it blocks the whole temporal
app suite from type-checking once the suite is enabled.

What:
Narrow the value at the 13 call sites with `msg as Message`, the cast idiom
already used elsewhere in these same files. Type-check only; no behaviour
change.
Why:
The workflow type-name behaviour had only Go-level coverage; the real Lua
application surface (process:spawn through a Temporal worker) was untested.

What:
Add a named_hello_workflow registered under an explicit string name
(meta.temporal.workflow.name) and a workflow_custom_name test that, via
spawn_monitored against the live worker, proves (a) a bare custom name
dispatches without a leading colon and (b) the workflow.name option overrides
the source id. Verified green against a real Temporal dev server
(temporal suite 41/41, app suite 423/423).
@wolfy-j wolfy-j merged commit 5fcf23c into main Jun 17, 2026
4 checks passed
@wolfy-j wolfy-j deleted the fix/fs-embed-lifecycle-hardening branch June 17, 2026 01:03
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.

2 participants