Repository navigation
provision infrastructure at deploy time, not on application start - #339
Merged
Merged
Conversation
adenhertog
force-pushed
the
issue-333-deploy-time-provisioning
branch
from
October 4, 2026 19:45
9f801d1 to
626f5ab
Compare
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
adenhertog
force-pushed
the
issue-333-deploy-time-provisioning
branch
from
October 6, 2026 10:10
626f5ab to
879b0e2
Compare
3 tasks done
This was referenced Oct 6, 2026
Closed
Closed
Open
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.
Closes #333
Summary
A bus no longer creates anything at
initialize():bus provision <module>(bus-cli) /bus.provision()create queues, topics, subscriptions, exchanges, tables and indexes at deploy time,initialize()verifies they exist with read-only calls, andwithAutoProvision()keeps startup provisioning for local dev and tests.Background
Every adapter provisioned in
initialize(), so every service needed permission to create and change its infrastructure, and teams using CDK/Terraform had no clean way to own it. The maintainer decided: refuse by default, document runtime permissions per adapter, and (groomed) verify on startup, CLI takes a configuration or factory, send-only buses provision topics for their message types, human-readable plan with--json.Problem
Verified against master fb4ede6 before changing anything:
initialize()created the DLQ, service queue, topics and subscriptions, always wrote the queue policy, and synced attributes;publishalso ranCreateTopicon first send.autoProvision: falseswitched these toGetQueueUrl/GetTopicAttributes/ListSubscriptionsByTopicchecks (topics only for handled messages).initialize()declared exchanges/queues/bindings, every first publish declared its exchange, retry queues were declared lazily inreturnMessage, and channel recovery re-declared everything.initialize()created the schema and the Delayed / deferred message delivery #258outgoing_messagestable + index;initializeWorkflow()created each workflow table and its indexes.initialize()created theoutgoingmessagesindex;initializeWorkflow()created each collection and its indexes.Approach
provisioning/addsProvisioningPlan(adapter,resources,runtimePermissions),ProvisionOptionsandResourcesNotProvisioned(names each missing resource;helppoints atbus provision,withAutoProvision()andwithResourceVerification(false)).Transport.provision?()andPersistence.provision?()are new;TransportInitializationOptionsgainsmessageNames,verifyResources,autoProvision;Persistence.initialize(options)gets{ workflows, verifyResources }andinitializeWorkflowis removed (WorkflowRegistrynow just recordsgetPersistedWorkflows()).BusInstance.provision({ dryRun })registers workflows, provisions persistence then transport (connecting once), never initializes.BusConfiguration.withAutoProvision()/withResourceVerification(verify).autoProvisionoption removed.provision()creates the bus' topics (every message name), DLQ, queue, subscriptions (custom-handler topics are only subscribed to, never created), sets the queue policy (only here) and syncs attributes; dry run makes no calls; plan includes an IAM runtime policy (sns:Publishon its topics, or every topic for a scheduler).initialize()checks what the bus receives through (queues, subscriptions, at most 10 calls at once; an auth error on an external topic is a warning); opt-inverifyQueuePolicychecks the queue policy.publishonly creates topics when auto-provisioned, and not again for provisioned ones.provision()declares the whole topology, including all 33 retry bucket queues up front. Queues are declared from one definition (durable+ exactx-arguments) that the plan lists as is, so tooling declaring from the plan gets identical queues.initialize()checks what it receives through with passive declares on throwaway channels. Without auto-provisioning, first publish to an exchange / first use of a retry queue checks it exists (so messages aren't silently dropped) instead of declaring; recovery only re-declares when auto-provisioned. Plan includesconfigure: ^$,writeonamq.default+ message exchanges (.*for a scheduler),readon the service, legacy retry and dead letter queues and^<queue>-retry-\d+ms$(what RabbitMQ 4.3.1+ needs for the passive checks; the<queue>/<queue>-retryexchanges share those names). A refused check throwsRabbitMqResourceCheckRefused;withResourceVerification(false)skips the startup and first-use checks alike.provision()(no change to the SQL/index logic);initialize()verifies (Postgres:pg_namespace+ oneto_regclassquery, legacy index names count; Mongo:listCollections/listIndexes). Plans include SQL grants / role privileges.bus provision <module> [--export name] [--dry-run] [--permissions] [--json]. Loads with nativeimport()(JS, or TS Node can type-strip), accepts aBusConfigurationor sync/async factory, builds,provision()s, always disposes.--jsonemits a versionedProvisionReport(exported type, documented).bus.provision()theninitialize()without provisioning, so every adapter is tested provision-then-run with verification on. Every other test bus on real infra uses.withAutoProvision(); adapters have new tests for missing resources, dry runs and plans./guide/provisioningpage (sidebar,snippets/provisioning.ts, CLI flags, JSON format); each adapter page has a Provisioning section with deploy-time and runtime permissions (incl. describe permissions) and the SQS queue policy; quick starts opt in withwithAutoProvision(); custom adapter pages/snippets updated. Minor changeset with Breaking: note and MIGRATING.md entries.Decisions for review
withAutoProvision()andwithResourceVerification(verify: boolean)(defaulttrue); errorResourcesNotProvisioned.withMessageTypes()entry, excluding workflow state (registered state names, or classes extendingWorkflowState, matched by base class name too for another copy of bus-core). Startup checks only what the bus receives through (queues, handled topics/exchanges, subscriptions); topics a bus only sends to aren't checked, and a send-only bus checks nothing on its transport.Persistence.initializeWorkflowremoved in favour ofinitialize({ workflows, verifyResources })+provision({ workflows, dryRun })(breaking for custom persistences; no shim).configure.sendsAnyMessageonTransportProvisionOptions); on SQS it's narrowed toresolveTopicName's fixed prefix when probing shows the resolver is prefix + default name, otherwise*. Buses sharing a store that aren't schedulers only get their own topics; the docs say to grant the others' or use a scheduler. A send-only bus with no message types logs a warning when provisioned.ResourcesNotProvisioned; SQS just publishes and lets SNS reject a missing topic. WithwithAutoProvision()both still create missing topics/exchanges lazily (today's dev behaviour, e.g. send-only buses without message types).verifyQueuePolicy). Indexes are verified for Postgres (legacy-named indexes count only with matching keys) and Mongo.readcovers them by default and the docs say it can be narrowed to the service queue on older brokers. A 403 throws a new bus-rabbitmq error,RabbitMqResourceCheckRefused(notResourcesNotProvisioned, since the resource may exist).iam-policy,rabbitmq-permissions,sql(with a<runtime_role>placeholder),mongodb-privileges. Replies to other services' SQS queues aren't in the generated IAM policy (unknown targets); documented.Review fixes
RabbitMQ plan queues carry their exact arguments (reproduced: without
x-dead-letter-routing-key: ''a retry from a plan-declared queue is dropped; new integration test declares from the plan, round-trips a retry and then auto-provisions against the same queues), narrower RabbitMQ permissions, scheduler grants, external SQS topics only subscribed, bounded SQS checks and opt-in policy check,registerWorkflowscaches its promise so a failure is rethrown not skipped, CLI reports dispose failures as a warning without hiding the provisioning error, Postgres verification compares a legacy index's keys, MIGRATING note for send-onlywithMessageTypes(). Re-review: RabbitMQ 4.3.1 check permissions andRabbitMqResourceCheckRefused, first-use checks honourwithResourceVerification(false), SQS scheduler wildcard narrowed to a topic prefix,verifyQueuePolicydocs say it only checks a policy exists. Rebased onto master 3e30f9b (#338).Rebase
Rebased onto master b72419b (#335 transactional outbox, after #338): #335's
initializeWorkflowuses moved toinitialize({ workflows })(the in-memory workflow buckets are still created there), the outgoing table'sdestinationcolumn is created byprovision(),outboxTestsprovisions then initializes,postgres-transaction.integration.tsuseswithAutoProvision(), and the Postgres grants cover the outbox's transactions (they run the same statements; tables handlers write withpostgresTransaction(ctx)are theirs to grant).🤖 Generated with Claude Code