Extract package runtime lane into kody-runtime worker (ADR 0016) - #1384
Conversation
Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…fixes Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
🔎 Preview deployed: https://kody-pr-1384.kody-a99.workers.dev Worker: Mocks:
|
…ce-routes) Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…ime-worker-extraction
|
bugbot review |
|
@coderabbitai review |
Bugbot couldn't run — GitHub account mismatchThe GitHub account linked to your Cursor account does not match the PR author. Please ensure you're using the correct GitHub account, or run Bugbot from a team that covers this repository. |
✅ Action performedReview finished.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR extracts package runtime execution into the ChangesRuntime Worker extraction
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant DeployWorkflow
participant ConfigGenerator
participant Cloudflare
participant RuntimeWorker
DeployWorkflow->>ConfigGenerator: generate runtime and bootstrap configurations
ConfigGenerator->>DeployWorkflow: return generated paths and bindings
DeployWorkflow->>Cloudflare: synchronize shared secrets
DeployWorkflow->>Cloudflare: deploy kody-runtime
DeployWorkflow->>Cloudflare: deploy kody
DeployWorkflow->>RuntimeWorker: GET /__runtime/health
RuntimeWorker->>DeployWorkflow: return status and commit SHA
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (7)
tools/ci/runtime-worker-config.ts (3)
337-344: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGuard against multiple workflow entries receiving the same name.
The loop assigns
${runtimeWorkerName}-dynamic-callable-workflowsto every entry inruntimeEnv.workflows. Today the runtime config declares one workflow, so the result is correct. If a second workflow is added later, both entries get the samenameand the deploy binds the wrong workflow. Derive the name from the entrybindingorclass_name, or fail when more than one entry exists.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/ci/runtime-worker-config.ts` around lines 337 - 344, Update the workflow-name assignment in the runtimeEnv.workflows loop so multiple entries cannot receive the same name. Derive each entry’s name from its binding or class_name while preserving the existing dynamic-callable naming for the current single-workflow configuration, or explicitly fail when more than one workflow entry is present.
186-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConfirm that overwriting
runtimeEnv.routesis intended.
addPackageAppRoutereplaces the wholeroutesarray with the single package-app entry. If the committed base config atpackages/runtime-worker/wrangler.jsoncever declares extra routes for an env, generation silently drops them. Appending, with a duplicate-pattern filter, matches the behavior ofaddPackageAppCustomDomainRouteintools/ci/resource-utils.ts.♻️ Proposed change
- runtimeEnv.routes = [{ pattern: hostname, custom_domain: true }] + const existingRoutes = Array.isArray(runtimeEnv.routes) + ? (runtimeEnv.routes as Array<unknown>) + : [] + const alreadyRouted = existingRoutes.some( + (route) => + route && + typeof route === 'object' && + (route as JsonRecord).pattern === hostname, + ) + runtimeEnv.routes = alreadyRouted + ? existingRoutes + : [...existingRoutes, { pattern: hostname, custom_domain: true }]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/ci/runtime-worker-config.ts` around lines 186 - 207, Update addPackageAppRoute to preserve existing runtimeEnv.routes entries instead of replacing the entire array. Append the package-app route while filtering any existing route with the same hostname/pattern, matching the duplicate-handling behavior of addPackageAppCustomDomainRoute; retain workers_dev handling unchanged.
268-303: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
parseArgsaccepts a value that looks like a flag.The pair loop reads
argv[index + 1]without checking for a leading--. A missing value silently consumes the next flag as the value, for example--env --out-config x. The command then fails later with a confusing message, or generates a config for env--out-config. Add a check that the value does not start with--.🛡️ Proposed fix
- if (!flag?.startsWith('--') || value === undefined) { + if (!flag?.startsWith('--') || value === undefined || value.startsWith('--')) { fail(`Invalid argument pair: ${flag ?? ''} ${value ?? ''}`) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/ci/runtime-worker-config.ts` around lines 268 - 303, Update parseArgs so each option value is rejected when it starts with “--”, alongside the existing invalid-pair validation. Preserve the current fail behavior and ensure missing values cannot be consumed as the next flag.wrangler-env.ts (2)
43-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse
isDevCommand, and confirm the path passed to wrangler resolves the same way as the existence check.Two points:
- Line 52 repeats
args[0] === 'dev'.isDevCommandat line 25 already holds this value. Use it.- The existence check resolves
runtimeWorkerConfigPaththroughresolveWranglerConfigPath(..., process.cwd()), but line 58 pushes the raw relative path. IfresolveWranglerConfigPathanchors to the repository root rather than the cwd,wranglerreceives a path that does not exist when the script runs from a subdirectory. Push the resolved path.♻️ Proposed change
- if ( - args[0] === 'dev' && - envName !== 'test' && - existsSync( - resolveWranglerConfigPath(runtimeWorkerConfigPath, process.cwd()), - ) - ) { - commandArgs.push('--config', runtimeWorkerConfigPath) - } + const resolvedRuntimeWorkerConfigPath = resolveWranglerConfigPath( + runtimeWorkerConfigPath, + process.cwd(), + ) + if ( + isDevCommand && + envName !== 'test' && + existsSync(resolvedRuntimeWorkerConfigPath) + ) { + commandArgs.push('--config', resolvedRuntimeWorkerConfigPath) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@wrangler-env.ts` around lines 43 - 59, Update the runtime-worker config condition to reuse the existing isDevCommand symbol instead of checking args[0] directly, while preserving the envName !== 'test' guard. Resolve runtimeWorkerConfigPath once with resolveWranglerConfigPath and use that same resolved value for both existsSync and the --config argument.
71-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUse a path-aware runtime-worker check.
getArgValuesupports both--config <path>and--config=<path>. Replace the substring check with a normalized path check for thepackages/runtime-worker/directory, including generated configs such aswrangler-production.generated.json; do not compare only withruntimeWorkerConfigPath.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@wrangler-env.ts` around lines 71 - 75, Update the isRuntimeWorkerConfig calculation near getArgValue to normalize the resolved --config path and detect whether it belongs to the packages/runtime-worker/ directory, supporting both --config argument forms and generated filenames such as wrangler-production.generated.json. Do not rely on substring matching or compare only against runtimeWorkerConfigPath; preserve the existing isWorkerBuildCommand condition.tools/ci/runtime-worker-config.node.test.ts (1)
252-256: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe migration assertion couples to the first committed migration tag.
runtimeConfig.migrations?.[0]?.tagmust be'v1'. If a migration is later inserted before the transfer migration, or the tag is renamed, this test fails for a reason unrelated to generation. Assert that some migration entry carriestransferred_classesinstead of indexing position 0.♻️ Proposed change
- expect(runtimeConfig.migrations?.[0]?.tag).toBe('v1') - expect( - Array.isArray(runtimeConfig.migrations?.[0]?.transferred_classes), - ).toBe(true) + const transferMigration = runtimeConfig.migrations?.find((migration) => + Array.isArray(migration.transferred_classes), + ) + expect(transferMigration).toBeDefined()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/ci/runtime-worker-config.node.test.ts` around lines 252 - 256, Update the migration assertions in the runtime configuration test to search runtimeConfig.migrations for an entry containing transferred_classes, rather than assuming the transfer migration is at index 0 or asserting its tag. Preserve the requirement that the matching entry exposes transferred_classes as an array.tools/check-deploy-guardrails.node.test.ts (1)
139-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a case for a malformed
transferred_classesentry.The test covers matching, retargeted, and unrecorded migrations. It does not cover the
normalizeTransferredClassesnull path. An entry with a missing or non-stringfrom_scriptmust still produce the "was removed, renamed, or changed" error. Add that case to lock the validation behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/check-deploy-guardrails.node.test.ts` around lines 139 - 204, Extend the test case around checkDurableObjectConfig with a malformed transferred_classes entry whose from_script is missing or non-string, and assert it produces the existing "protected transferred_classes migration \"v1\" was removed, renamed, or changed" error. Keep the matching, retargeted, and unrecorded migration assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/deploy.yml:
- Around line 202-234: Define and apply one shared allowlist of runtime-lane
secrets across both sync paths. In .github/workflows/deploy.yml:202-234, update
the runtime sync env block and command to remove STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET, KIT_API_KEY, and all OAuth client ID/secret pairs,
retaining only secrets consumed by the runtime lane. In
.github/workflows/preview.yml:344-354, replace the dotenv-based runtime sync
with explicit --set or --set-from-env-optional entries for exactly the same
allowlisted secrets.
- Around line 380-416: In both runtime healthcheck blocks, add the existing
fail-fast guard for EXPECTED_COMMIT_SHA before polling begins: validate that the
environment value is non-empty, print an error to stderr, and exit 1 when
missing. Apply this to .github/workflows/deploy.yml lines 380-416 and
.github/workflows/preview.yml lines 423-458; leave the status and commitSha
response checks unchanged.
- Around line 243-263: Update the runtime deploy step identified by
deploy_runtime to reuse the shared is_retryable_deploy_failure helper and
deployment-attempt loop used by the main deploy, including retries for
workflows.api.error.internal_server and Cloudflare code 10001. Before extracting
the URL, remove ANSI escape sequences from deploy-runtime.log, then match the
URL specifically on RUNTIME_WORKER_NAME rather than selecting the last
workers.dev URL; preserve the existing GitHub output behavior.
- Around line 243-257: Add a handover step before the “Deploy runtime worker to
Cloudflare Workers” step that explicitly removes the kodyapps.dev custom domain
from the kody Worker using the existing Cloudflare credentials and API
conventions. Ensure the handover completes before the runtime deployment runs,
while preserving the current deployment command and configuration.
In `@docs/contributing/architecture/runtime-worker-migration-runbook.md`:
- Around line 108-122: Complete the manual execution procedure by adding the
equivalent secret synchronization for both Workers and the D1 migration step
before deployment, reusing the commands or symbols from the automated procedure.
Alternatively, state explicit verified preconditions requiring both operations
to finish before either deploy command runs.
- Around line 90-98: Update the runtime-worker migration runbook around the
consecutive kody-runtime and kody deployment steps to require quiescing or
gating runtime traffic before the transferred_classes migration. Document that
the gate must remain active through both deployments and be released only after
the main kody deployment and its healthchecks succeed, covering both the
workflow and manual procedure.
In `@packages/runtime-worker/wrangler.jsonc`:
- Around line 52-78: Update packages/runtime-worker/wrangler.jsonc lines 52-78
to ensure the transferred Durable Object classes match the exports from
packages/worker/src/runtime-worker.ts, and document the required deployment
order in the runbook. In packages/worker/wrangler.jsonc lines 255-292, verify
that packages/worker/src/index.ts still exports StorageRunner, RunLog,
PackageRealtimeSession, and PackageServiceInstance; if not, add migration
entries that retire those classes from the kody script so every declared
migration class is exported by its uploaded entry module.
- Around line 94-246: Add an `EMAIL_BLOBS` R2 bucket binding to the runtime
Worker's R2 configuration for both runtime environments, using the existing
email-blob bucket name and binding conventions. Ensure package-invocation code
can access the bucket through `env.EMAIL_BLOBS` in each environment.
In `@packages/worker/worker-configuration.d.ts`:
- Around line 58-67: Add the explicit RPC contracts currently represented by
local as unknown as casts for STORAGE_RUNNER and RUN_LOG to a shared runtime
interface, then type both DurableObjectNamespace declarations with that
interface. Remove the local casts and ensure the shared interface exposes all
RPC methods and payload types so signature changes remain type-checked.
In `@tools/check-deploy-guardrails.ts`:
- Around line 265-302: Update checkDurableObjectConfig and its migration
collection to include migrations from every config.env.<name> section, while
keeping duplicate-tag validation scoped per section so environment-specific tags
such as v1 may be reused. Add the preview v1 new_sqlite_classes migration for
packages/runtime-worker/wrangler.jsonc to tools/ci/durable-object-baseline.json,
ensuring it is protected by the baseline.
---
Nitpick comments:
In `@tools/check-deploy-guardrails.node.test.ts`:
- Around line 139-204: Extend the test case around checkDurableObjectConfig with
a malformed transferred_classes entry whose from_script is missing or
non-string, and assert it produces the existing "protected transferred_classes
migration \"v1\" was removed, renamed, or changed" error. Keep the matching,
retargeted, and unrecorded migration assertions unchanged.
In `@tools/ci/runtime-worker-config.node.test.ts`:
- Around line 252-256: Update the migration assertions in the runtime
configuration test to search runtimeConfig.migrations for an entry containing
transferred_classes, rather than assuming the transfer migration is at index 0
or asserting its tag. Preserve the requirement that the matching entry exposes
transferred_classes as an array.
In `@tools/ci/runtime-worker-config.ts`:
- Around line 337-344: Update the workflow-name assignment in the
runtimeEnv.workflows loop so multiple entries cannot receive the same name.
Derive each entry’s name from its binding or class_name while preserving the
existing dynamic-callable naming for the current single-workflow configuration,
or explicitly fail when more than one workflow entry is present.
- Around line 186-207: Update addPackageAppRoute to preserve existing
runtimeEnv.routes entries instead of replacing the entire array. Append the
package-app route while filtering any existing route with the same
hostname/pattern, matching the duplicate-handling behavior of
addPackageAppCustomDomainRoute; retain workers_dev handling unchanged.
- Around line 268-303: Update parseArgs so each option value is rejected when it
starts with “--”, alongside the existing invalid-pair validation. Preserve the
current fail behavior and ensure missing values cannot be consumed as the next
flag.
In `@wrangler-env.ts`:
- Around line 43-59: Update the runtime-worker config condition to reuse the
existing isDevCommand symbol instead of checking args[0] directly, while
preserving the envName !== 'test' guard. Resolve runtimeWorkerConfigPath once
with resolveWranglerConfigPath and use that same resolved value for both
existsSync and the --config argument.
- Around line 71-75: Update the isRuntimeWorkerConfig calculation near
getArgValue to normalize the resolved --config path and detect whether it
belongs to the packages/runtime-worker/ directory, supporting both --config
argument forms and generated filenames such as
wrangler-production.generated.json. Do not rely on substring matching or compare
only against runtimeWorkerConfigPath; preserve the existing isWorkerBuildCommand
condition.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e1a4186f-8b55-4578-9cf3-71469dbd338f
📒 Files selected for processing (24)
.github/workflows/deploy.yml.github/workflows/preview.ymldocs/contributing/architecture/index.mddocs/contributing/architecture/runtime-worker-migration-runbook.mdpackage.jsonpackages/runtime-worker/project.jsonpackages/runtime-worker/readme.mdpackages/runtime-worker/wrangler.jsoncpackages/shared/src/runtime-worker.tspackages/worker/src/env-schema.tspackages/worker/src/index.tspackages/worker/src/runtime-worker-routing.tspackages/worker/src/runtime-worker.tspackages/worker/worker-configuration.d.tspackages/worker/wrangler.jsonctools/check-deploy-guardrails.node.test.tstools/check-deploy-guardrails.tstools/ci/durable-object-baseline.jsontools/ci/preview-resources.tstools/ci/resource-utils.node.test.tstools/ci/resource-utils.tstools/ci/runtime-worker-config.node.test.tstools/ci/runtime-worker-config.tswrangler-env.ts
| - name: 🔐 Sync Cloudflare Secrets to runtime worker (bulk) | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| WRANGLER_CONFIG: | ||
| ${{ steps.runtime_config.outputs.runtime_wrangler_config }} | ||
| COOKIE_SECRET: ${{ secrets.COOKIE_SECRET }} | ||
| SECRET_STORE_KEY: ${{ secrets.SECRET_STORE_KEY }} | ||
| AI_GATEWAY_ID: ${{ secrets.AI_GATEWAY_ID }} | ||
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | ||
| CAPABILITY_REINDEX_SECRET: ${{ secrets.CAPABILITY_REINDEX_SECRET }} | ||
| GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }} | ||
| GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} | ||
| GOOGLE_CLIENT_ID: ${{ secrets.OAUTH_GOOGLE_CLIENT_ID }} | ||
| GOOGLE_CLIENT_SECRET: ${{ secrets.OAUTH_GOOGLE_CLIENT_SECRET }} | ||
| X_CLIENT_ID: ${{ secrets.OAUTH_X_CLIENT_ID }} | ||
| X_CLIENT_SECRET: ${{ secrets.OAUTH_X_CLIENT_SECRET }} | ||
| KIT_API_KEY: ${{ secrets.KIT_API_KEY }} | ||
| STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} | ||
| STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }} | ||
| run: > | ||
| node tools/ci/sync-worker-secrets.ts --env production --config | ||
| "$WRANGLER_CONFIG" --set-from-env COOKIE_SECRET | ||
| --set-from-env-optional SECRET_STORE_KEY --set-from-env-optional | ||
| AI_GATEWAY_ID --set-from-env-optional SENTRY_DSN | ||
| --set-from-env-optional CLOUDFLARE_API_TOKEN --set-from-env-optional | ||
| CAPABILITY_REINDEX_SECRET --set-from-env-optional GITHUB_CLIENT_ID | ||
| --set-from-env-optional GITHUB_CLIENT_SECRET --set-from-env-optional | ||
| GOOGLE_CLIENT_ID --set-from-env-optional GOOGLE_CLIENT_SECRET | ||
| --set-from-env-optional X_CLIENT_ID --set-from-env-optional | ||
| X_CLIENT_SECRET --set-from-env-optional KIT_API_KEY | ||
| --set-from-env-optional STRIPE_SECRET_KEY --set-from-env-optional | ||
| STRIPE_WEBHOOK_SECRET | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
The runtime Worker receives the main Worker's full secret set in both workflows. Both secret-sync paths copy every application secret to kody-runtime and to ${APP_WORKER_NAME}-runtime, although the runtime handler in packages/worker/src/runtime-worker.ts only serves package apps, package invocation, and the health path. Billing, waiting-list, and OAuth flows stay on the main Worker, so the extra secrets widen the blast radius of a runtime-Worker compromise without a functional need. Define one shared allowlist of runtime-lane secrets and use it in both places.
.github/workflows/deploy.yml#L202-L234: dropSTRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,KIT_API_KEY, and the OAuth client id/secret pairs from the runtime sync command and itsenvblock; keep only the secrets that the runtime lane reads..github/workflows/preview.yml#L344-L354: replace the second--from-dotenv "$ENV_FILE" --from-dotenv "$OVERRIDES_FILE"sync for$RUNTIME_WORKER_NAMEwith an explicit--set/--set-from-env-optionallist limited to the same runtime-lane secrets.
📍 Affects 2 files
.github/workflows/deploy.yml#L202-L234(this comment).github/workflows/preview.yml#L344-L354
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy.yml around lines 202 - 234, Define and apply one
shared allowlist of runtime-lane secrets across both sync paths. In
.github/workflows/deploy.yml:202-234, update the runtime sync env block and
command to remove STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, KIT_API_KEY, and all
OAuth client ID/secret pairs, retaining only secrets consumed by the runtime
lane. In .github/workflows/preview.yml:344-354, replace the dotenv-based runtime
sync with explicit --set or --set-from-env-optional entries for exactly the same
allowlisted secrets.
There was a problem hiding this comment.
Fixed in 290f2d0. The production runtime sync now uses a strict runtime-lane allowlist: COOKIE_SECRET, SECRET_STORE_KEY, AI_GATEWAY_ID, SENTRY_DSN, CLOUDFLARE_API_TOKEN — Stripe, Kit, OAuth client pairs, and CAPABILITY_REINDEX_SECRET no longer reach kody-runtime (removed from both the env block and the command). Preview intentionally keeps the dotenv-based sync (see the new comment in preview.yml): those files contain only .env.example placeholders plus per-PR mock-endpoint overrides that runtime capability execution needs, and no production secret is present in either file.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| # Deploys before the main worker: the main worker's cross-script | ||
| # Durable Object / service bindings require the kody-runtime script to | ||
| # exist. On the first production deploy this also runs the | ||
| # transferred_classes migration; see | ||
| # docs/contributing/architecture/runtime-worker-migration-runbook.md. | ||
| - name: ☁️ Deploy runtime worker to Cloudflare Workers | ||
| id: deploy_runtime | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| DEPLOY_COMMIT_SHA: ${{ needs.sha-guard.outputs.deploy_sha }} | ||
| WRANGLER_CONFIG: | ||
| ${{ steps.runtime_config.outputs.runtime_wrangler_config }} | ||
| run: | | ||
| set -euo pipefail | ||
| npm run deploy -- --config "$WRANGLER_CONFIG" --var "APP_COMMIT_SHA:${DEPLOY_COMMIT_SHA}" 2>&1 | tee deploy-runtime.log |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Look for custom-domain handover steps in the runtime worker migration runbook.
fd -t f 'runtime-worker-migration-runbook.md' --exec rg -n -C 4 -i 'custom domain|kodyapps|detach|route'Repository: kentcdodds/kody
Length of output: 2483
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- workflow deployment context ---'
sed -n '200,290p' .github/workflows/deploy.yml
echo '--- migration runbook ---'
fd -t f 'runtime-worker-migration-runbook.md' --exec sed -n '1,180p' {}
echo '--- custom-domain configuration and migration references ---'
rg -n -C 4 -i 'kodyapps\.dev|custom[_ -]?domain|customDomain|detach|transfer|runtime-worker' \
.github tools docs wrangler.toml '*.json' '*.yml' '*.yaml' 2>/dev/null || trueRepository: kentcdodds/kody
Length of output: 50374
🌐 Web query:
Cloudflare Workers custom domain already assigned to another Worker deploy transfer ownership existing custom domain Wrangler
💡 Result:
When you encounter the error stating that a custom domain is already assigned to another Cloudflare Worker, you cannot automatically transfer ownership via a single Wrangler command [1][2]. Cloudflare prevents multiple workers from being assigned to the same custom domain or route to ensure routing stability [1][2]. To resolve this and move the domain to your desired Worker, you must manually unassign the custom domain from the existing Worker first [1][2]. Steps to transfer the domain: 1. Identify the worker currently assigned to the domain: The Wrangler error message typically identifies the worker name and provides a link to the Cloudflare dashboard [1][3]. 2. Unassign the domain: Log in to the Cloudflare dashboard, navigate to the Workers overview page or the specific worker currently using the domain, and remove the custom domain or route association [1][2]. 3. Deploy your new worker: Once the domain is unassigned, run your wrangler deploy command again [1][2]. The new worker will now be able to claim the custom domain successfully [1]. There is currently no native "override" or "force transfer" flag in Wrangler to automate this process because the Cloudflare API requires explicit unassignment to prevent configuration conflicts [4][2][5]. If you are managing many workers, you may need to design your workflow to explicitly unassign routes as part of your CI/CD process before deploying the new version [2]. For advanced scenarios where you need to route many domains, consider Cloudflare for Platforms (formerly Cloudflare for SaaS), which allows for more flexible programmatic hostname management [6].
Citations:
- 1: Display a more helpful error when publishing to a route in use by another worker cloudflare/workers-sdk#2023
- 2: Can't deploy a worker to routes that are assigned to another worker. cloudflare/wrangler-action#241
- 3: Version Packages cloudflare/workers-sdk#2005
- 4:
override_existing_dns_recordflag ignored inwrangler deploycloudflare/workers-sdk#9878 - 5: Errors cloudflare_worker_route on deploying cloudflare worker, still deployed cloudflare/terraform-provider-cloudflare#1893
- 6: https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/hostname-routing/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
workflow = Path(".github/workflows/deploy.yml").read_text()
runbook = Path("docs/contributing/architecture/runtime-worker-migration-runbook.md").read_text()
runtime_config = Path("tools/ci/runtime-worker-config.ts").read_text()
resource_utils = Path("tools/ci/resource-utils.ts").read_text()
checks = {
"runtime deploy precedes main deploy": workflow.index(
"Deploy runtime worker to Cloudflare Workers"
) < workflow.index("Deploy to Cloudflare Workers"),
"runbook has no custom-domain handover command": not any(
term in runbook.lower()
for term in ("detach", "unassign", "custom domain handover", "remove custom domain")
),
"runtime config attaches package-app host": "runtimeEnv.routes = [{ pattern: hostname, custom_domain: true }]" in runtime_config,
"main config excludes package-app host": "package-app host itself is attached to the runtime Worker" in resource_utils,
"workflow has no explicit custom-domain removal": not any(
term in workflow.lower()
for term in ("delete custom domain", "remove custom domain", "unassign", "workers/domains")
),
}
for name, result in checks.items():
print(f"{name}: {'yes' if result else 'no'}")
PYRepository: kentcdodds/kody
Length of output: 386
Add an explicit kodyapps.dev handover before deploying kody-runtime.
The runbook and workflow do not remove the domain from kody. Cloudflare rejects assigning a custom domain that another Worker owns, so the first production deploy fails unless the workflow unassigns kodyapps.dev before the runtime deploy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy.yml around lines 243 - 257, Add a handover step
before the “Deploy runtime worker to Cloudflare Workers” step that explicitly
removes the kodyapps.dev custom domain from the kody Worker using the existing
Cloudflare credentials and API conventions. Ensure the handover completes before
the runtime deployment runs, while preserving the current deployment command and
configuration.
There was a problem hiding this comment.
Valid — fixed in the runbook (290f2d0). Cloudflare refuses to attach a custom domain that another Worker still owns, so the first production kody-runtime deploy would fail while kody holds kodyapps.dev. The runbook now has a dedicated step: detach kodyapps.dev from kody in the dashboard immediately before merging, with the explicit note that package-app traffic on that domain is unserved between the detach and the runtime deploy's attach (one-time only; later deploys find the domain already on kody-runtime).
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| # Deploys before the main worker: the main worker's cross-script | ||
| # Durable Object / service bindings require the kody-runtime script to | ||
| # exist. On the first production deploy this also runs the | ||
| # transferred_classes migration; see | ||
| # docs/contributing/architecture/runtime-worker-migration-runbook.md. | ||
| - name: ☁️ Deploy runtime worker to Cloudflare Workers | ||
| id: deploy_runtime | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| DEPLOY_COMMIT_SHA: ${{ needs.sha-guard.outputs.deploy_sha }} | ||
| WRANGLER_CONFIG: | ||
| ${{ steps.runtime_config.outputs.runtime_wrangler_config }} | ||
| run: | | ||
| set -euo pipefail | ||
| npm run deploy -- --config "$WRANGLER_CONFIG" --var "APP_COMMIT_SHA:${DEPLOY_COMMIT_SHA}" 2>&1 | tee deploy-runtime.log | ||
|
|
||
| RUNTIME_URL="$(node -e "const fs = require('node:fs'); const t = fs.readFileSync('deploy-runtime.log','utf8'); const m = t.match(/https:\\/\\/[a-zA-Z0-9._-]+\\.workers\\.dev/g); process.stdout.write(m?.at(-1) ?? '')")" | ||
| if [ -n "$RUNTIME_URL" ]; then | ||
| echo "url=$RUNTIME_URL" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
The runtime deploy lacks the retry and ANSI handling that the main deploy has.
Three defects in this step:
- No retry. The main deploy at lines 275-313 retries
workflows.api.error.internal_serverand Cloudflarecode: 10001. The runtime Worker also creates a Workflow and Durable Object namespaces, so it faces the same transient failures. A single transient error fails the production deploy before the main Worker is deployed. - The URL extraction at line 259 does not strip ANSI escape codes.
.github/workflows/preview.ymlline 284 strips them before matching. If wrangler colorizes the URL, the character class[a-zA-Z0-9._-]+stops at the escape sequence and the match is empty or truncated.RUNTIME_URLis then unset, and the runtime healthcheck at lines 388-391 exits 1 withMissing runtime worker deploy URL output. - The extraction takes the last
*.workers.devmatch in the log without anchoring to the runtime Worker name. The preview step anchors on$RUNTIME_WORKER_NAME. An unrelated URL printed by wrangler produces a healthcheck against the wrong host.
🐛 Proposed fix for the URL extraction
- RUNTIME_URL="$(node -e "const fs = require('node:fs'); const t = fs.readFileSync('deploy-runtime.log','utf8'); const m = t.match(/https:\\/\\/[a-zA-Z0-9._-]+\\.workers\\.dev/g); process.stdout.write(m?.at(-1) ?? '')")"
+ RUNTIME_URL="$(node -e 'const fs = require("node:fs"); const text = fs.readFileSync("deploy-runtime.log", "utf8").replace(/\u001b\[[0-9;]*m/g, ""); const worker = process.argv[1]; const escaped = worker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const workerUrlRegex = new RegExp(`https://(?:[a-zA-Z0-9-]+\\.)?${escaped}\\.[a-zA-Z0-9._-]+\\.workers\\.dev`, "g"); const matches = text.match(workerUrlRegex); process.stdout.write(matches?.at(-1) ?? "")' kody-runtime)"For the retry, extract the is_retryable_deploy_failure helper and the attempt loop into a shared script and call it from both deploy steps.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy.yml around lines 243 - 263, Update the runtime
deploy step identified by deploy_runtime to reuse the shared
is_retryable_deploy_failure helper and deployment-attempt loop used by the main
deploy, including retries for workflows.api.error.internal_server and Cloudflare
code 10001. Before extracting the URL, remove ANSI escape sequences from
deploy-runtime.log, then match the URL specifically on RUNTIME_WORKER_NAME
rather than selecting the last workers.dev URL; preserve the existing GitHub
output behavior.
There was a problem hiding this comment.
All three fixed in 290f2d0: (1) the runtime deploy now retries up to 3 attempts with exponential backoff on the same retryable signatures as the main deploy (workflows.api.error.internal_server, Cloudflare code: 10001), checked against an ANSI-stripped log; (2) URL extraction strips ANSI escapes before matching; (3) the workers.dev URL regex is anchored on the kody-runtime worker name (passed as an argument), matching the preview step's approach.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| - name: 🩺 Healthcheck runtime worker (production) | ||
| shell: bash | ||
| env: | ||
| RUNTIME_URL: ${{ steps.deploy_runtime.outputs.url }} | ||
| EXPECTED_COMMIT_SHA: ${{ needs.sha-guard.outputs.deploy_sha }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [ -z "${RUNTIME_URL:-}" ]; then | ||
| echo "Missing runtime worker deploy URL output; cannot run healthcheck." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| HEALTHCHECK_URL="${RUNTIME_URL%/}/__runtime/health" | ||
| echo "Healthcheck URL: $HEALTHCHECK_URL" | ||
|
|
||
| attempts=20 | ||
| delay_seconds=3 | ||
|
|
||
| for i in $(seq 1 "$attempts"); do | ||
| echo "Attempt $i/$attempts" | ||
| if curl --fail --silent --show-error --location --max-time 10 \ | ||
| --header "Accept: application/json" \ | ||
| "$HEALTHCHECK_URL" > runtime-health.json && \ | ||
| node -e "const fs = require('node:fs'); const json = JSON.parse(fs.readFileSync('runtime-health.json','utf8')); const expected = process.env.EXPECTED_COMMIT_SHA; if (json?.status !== 'ok') { console.error('runtime-healthcheck-unexpected-response', json); process.exit(1); } if (json?.commitSha !== expected) { console.error('runtime-healthcheck-unexpected-commit-sha', { expected, actual: json?.commitSha }); process.exit(1); } console.log('runtime-healthcheck-ok', json);"; then | ||
| exit 0 | ||
| fi | ||
| sleep "$delay_seconds" | ||
| done | ||
|
|
||
| echo "Runtime worker healthcheck failed after ${attempts} attempts: $HEALTHCHECK_URL" >&2 | ||
| if [ -f runtime-health.json ]; then | ||
| echo "Last response body:" >&2 | ||
| cat runtime-health.json >&2 | ||
| fi | ||
| exit 1 | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect the shared runtime worker health payload contract.
fd -t f 'runtime-worker.ts' -p packages/shared --exec rg -n -C 8 'buildRuntimeWorkerHealth|runtimeWorkerHealthPath|status|commitSha'Repository: kentcdodds/kody
Length of output: 986
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for file in .github/workflows/deploy.yml .github/workflows/preview.yml; do
echo "===== $file: healthcheck context ====="
if [ "$file" = ".github/workflows/deploy.yml" ]; then
sed -n '330,420p' "$file"
echo "===== $file: SHA and runtime deployment references ====="
rg -n -C 5 'deploy_sha|EXPECTED_COMMIT_SHA|deploy_runtime|commitSha|sha-guard' "$file"
else
sed -n '400,465p' "$file"
echo "===== $file: SHA and runtime deployment references ====="
rg -n -C 5 'EXPECTED_COMMIT_SHA|deploy_runtime|commitSha|github\.sha|sha-guard' "$file"
fi
doneRepository: kentcdodds/kody
Length of output: 25344
Add fail-fast guards for EXPECTED_COMMIT_SHA. buildRuntimeWorkerHealth emits status and commitSha, so both field checks are correct. Add the guard used by the main healthchecks before polling in both runtime healthchecks. An empty expected SHA currently consumes the full retry budget before failing.
📍 Affects 2 files
.github/workflows/deploy.yml#L380-L416(this comment).github/workflows/preview.yml#L423-L458
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy.yml around lines 380 - 416, In both runtime
healthcheck blocks, add the existing fail-fast guard for EXPECTED_COMMIT_SHA
before polling begins: validate that the environment value is non-empty, print
an error to stderr, and exit 1 when missing. Apply this to
.github/workflows/deploy.yml lines 380-416 and .github/workflows/preview.yml
lines 423-458; leave the status and commitSha response checks unchanged.
There was a problem hiding this comment.
Fixed in 290f2d0: both runtime healthchecks (deploy.yml production and preview.yml) now fail fast with Missing expected commit SHA; cannot verify runtime deployment version. when EXPECTED_COMMIT_SHA is empty, before entering the polling loop — same guard the main healthchecks use.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| 4. **deploys `kody-runtime` first** — this applies the `v1` | ||
| `transferred_classes` migration, moving the four classes' storage out of | ||
| `kody`. From this moment the still-running old `kody` deployment serves | ||
| runtime traffic against namespaces that have moved; the window until step | ||
| 5 completes must be short and is why the two deploys are consecutive steps | ||
| in one job; | ||
| 5. **deploys `kody` second** with the config that binds the four classes | ||
| cross-script (`script_name: "kody-runtime"`) plus the `RUNTIME_WORKER` | ||
| service binding, and stops routing runtime requests in-process; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify that the production workflow prevents runtime traffic from reaching
# the old main Worker during the transferred_classes migration window.
rg -n -i -C 5 \
'maintenance|drain|quiesce|traffic|kody-runtime|transferred_classes|deploy' \
.github/workflows/deploy.ymlRepository: kentcdodds/kody
Length of output: 20622
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- runbook ---'
sed -n '35,115p' docs/contributing/architecture/runtime-worker-migration-runbook.md
printf '%s\n' '--- workflow deploy sequence and relevant controls ---'
sed -n '140,330p' .github/workflows/deploy.yml
printf '%s\n' '--- runtime migration references ---'
rg -n -i -C 4 \
'transferred_classes|runtime worker|cross-script|script_name|RUNTIME_WORKER|namespace|quiet|maintenance|drain|traffic' \
docs packages tools .github/workflows -g '*.md' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.json' -g '*.jsonc' -g '*.yml' -g '*.yaml'Repository: kentcdodds/kody
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- runbook excerpt ---'
sed -n '1,135p' docs/contributing/architecture/runtime-worker-migration-runbook.md
printf '%s\n' '--- routing implementation ---'
cat -n packages/worker/src/runtime-worker-routing.ts
sed -n '510,570p' packages/worker/src/index.ts
printf '%s\n' '--- exact migration and transfer references ---'
rg -l -i 'transferred_classes|transfer.*durable|durable.*transfer' docs packages tools .github/workflows \
-g '*.md' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.json' -g '*.jsonc' -g '*.yml' -g '*.yaml' |
while IFS= read -r file; do
echo "--- $file"
rg -n -i -C 5 'transferred_classes|transfer.*durable|durable.*transfer' "$file"
doneRepository: kentcdodds/kody
Length of output: 22498
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
workflow = Path(".github/workflows/deploy.yml").read_text()
runtime_step = workflow.index("Deploy runtime worker to Cloudflare Workers")
main_step = workflow.index("Deploy to Cloudflare Workers", runtime_step + 1)
before_runtime = workflow[:runtime_step]
gate_terms = re.findall(
r"(?i)\b(?:maintenance|drain|quiesc|traffic[-_ ]?gate|pause|read[-_ ]?only)\b",
before_runtime,
)
print("runtime_step_before_main_step:", runtime_step < main_step)
print("gate_terms_before_runtime_step:", gate_terms)
runbook = Path("docs/contributing/architecture/runtime-worker-migration-runbook.md").read_text()
print("runbook_explicitly_describes_old_worker_window:",
"still-running old `kody` deployment serves runtime traffic" in runbook)
print("runbook_explicitly_describes_old_namespace_failure:",
"DO requests routed through the old script's namespaces fail" in runbook)
PY
printf '%s\n' '--- runtime-owned route call sites ---'
rg -n -C 3 \
'STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS|RUNTIME_WORKER|isRuntimeWorkerOwnedRequest' \
packages/worker/src packages/runtime-worker/src packages/worker/wrangler.jsonc \
packages/runtime-worker/wrangler.jsoncRepository: kentcdodds/kody
Length of output: 50372
Quiesce runtime traffic before the transferred_classes migration.
The workflow and manual procedure deploy kody-runtime before kody, without an enforceable traffic gate. Requests from the old kody deployment can fail after the migration removes its Durable Object namespaces. Add a gate before the runtime deployment and release it only after the main Worker deploy and healthchecks complete.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/contributing/architecture/runtime-worker-migration-runbook.md` around
lines 90 - 98, Update the runtime-worker migration runbook around the
consecutive kody-runtime and kody deployment steps to require quiescing or
gating runtime traffic before the transferred_classes migration. Document that
the gate must remain active through both deployments and be released only after
the main kody deployment and its healthchecks succeed, covering both the
workflow and manual procedure.
There was a problem hiding this comment.
Addressed in the runbook (290f2d0) by documenting the accepted risk rather than adding a traffic gate: per ADR 0016 there is deliberately no maintenance-mode quiescing — runtime requests hitting the still-running old kody deployment during the seconds-long window between the two consecutive deploy steps error and surface as failed runs/invocations, which is why the deploy runs at a quiet time. Building a maintenance-mode gate would be a larger change than the extraction itself and would reverse the ADR's agreed cutover design.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| ## Manual execution (only if the workflow cannot run) | ||
|
|
||
| From a checkout of the merged commit, with `CLOUDFLARE_API_TOKEN` set: | ||
|
|
||
| ```sh | ||
| node tools/ci/production-resources.ts ensure --out-config packages/worker/wrangler-production.generated.json | ||
| node tools/ci/runtime-worker-config.ts generate \ | ||
| --env production \ | ||
| --main-config packages/worker/wrangler-production.generated.json \ | ||
| --worker-name kody-runtime \ | ||
| --main-worker-name kody \ | ||
| --out-config packages/runtime-worker/wrangler-production.generated.json | ||
| npm run deploy -- --config packages/runtime-worker/wrangler-production.generated.json | ||
| npm run deploy -- --config packages/worker/wrangler-production.generated.json | ||
| ``` |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Make the manual deployment procedure complete.
The automated procedure syncs secrets to both Workers and applies D1 migrations in Lines 85-90. The manual procedure in Lines 113-121 performs neither operation. If an operator uses this fallback, kody-runtime can deploy without its required secrets and shared database changes can remain unapplied.
Add the equivalent secret-sync and D1-migration steps, or state verified preconditions that require both operations to have completed before either deploy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/contributing/architecture/runtime-worker-migration-runbook.md` around
lines 108 - 122, Complete the manual execution procedure by adding the
equivalent secret synchronization for both Workers and the D1 migration step
before deployment, reusing the commands or symbols from the automated procedure.
Alternatively, state explicit verified preconditions requiring both operations
to finish before either deploy command runs.
There was a problem hiding this comment.
Fixed in 290f2d0: the manual-execution section now mirrors the workflow order — generate configs, sync secrets to both workers (tools/ci/sync-worker-secrets.ts, with the runtime-lane allowlist spelled out and deploy.yml named as the source of truth for the exact flag lists), apply the shared D1 migrations (wrangler-env.ts d1 migrations apply APP_DB/AUDIT_DB --remote), then deploy runtime → main.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| "migrations": [ | ||
| { | ||
| "tag": "v1", | ||
| "transferred_classes": [ | ||
| { | ||
| "from": "StorageRunner", | ||
| "from_script": "kody", | ||
| "to": "StorageRunner", | ||
| }, | ||
| { | ||
| "from": "RunLog", | ||
| "from_script": "kody", | ||
| "to": "RunLog", | ||
| }, | ||
| { | ||
| "from": "PackageRealtimeSession", | ||
| "from_script": "kody", | ||
| "to": "PackageRealtimeSession", | ||
| }, | ||
| { | ||
| "from": "PackageServiceInstance", | ||
| "from_script": "kody", | ||
| "to": "PackageServiceInstance", | ||
| }, | ||
| ], | ||
| }, | ||
| ], |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
The four Durable Object classes are transferred out of kody while kody still declares them. The runtime configuration transfers StorageRunner, RunLog, PackageRealtimeSession, and PackageServiceInstance from the kody script, but the main configuration still names those classes in its top-level new_sqlite_classes migrations, which env.test and env.preview inherit. Wrangler validates that every class named in a migration is exported by the uploaded entry module.
packages/runtime-worker/wrangler.jsonc#L52-L78: confirm the transfer targets match the classes thatpackages/worker/src/runtime-worker.tsexports, and record the required deploy order in the runbook.packages/worker/wrangler.jsonc#L255-L292: confirm thatpackages/worker/src/index.tsstill exports the four classes after the split, or plan the migration entries that retire them from thekodyscript.
📍 Affects 2 files
packages/runtime-worker/wrangler.jsonc#L52-L78(this comment)packages/worker/wrangler.jsonc#L255-L292
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/runtime-worker/wrangler.jsonc` around lines 52 - 78, Update
packages/runtime-worker/wrangler.jsonc lines 52-78 to ensure the transferred
Durable Object classes match the exports from
packages/worker/src/runtime-worker.ts, and document the required deployment
order in the runbook. In packages/worker/wrangler.jsonc lines 255-292, verify
that packages/worker/src/index.ts still exports StorageRunner, RunLog,
PackageRealtimeSession, and PackageServiceInstance; if not, add migration
entries that retire those classes from the kody script so every declared
migration class is exported by its uploaded entry module.
There was a problem hiding this comment.
Verified — no change needed. packages/worker/src/index.ts deliberately still exports StorageRunner, RunLog, PackageRealtimeSession, and PackageServiceInstance (needed for the test environment's in-process DO fallback), so wrangler's "every migration class must be exported by the entry module" validation holds for kody in all environments including env.test/env.preview, which inherit the top-level new_sqlite_classes history. That history must stay on kody: a transferred_classes migration requires the source script to retain the migrations that created the classes. The transfer targets match the classes runtime-worker.ts exports, and the deploy order (runtime first, main second) is in the runbook. Retiring the classes from kody (deleted-class migration + dropping the exports) is a deliberate post-cutover follow-up, noted in the runbook's rollback constraints.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| "worker_loaders": [ | ||
| { | ||
| "binding": "LOADER", | ||
| }, | ||
| { | ||
| "binding": "APP_LOADER", | ||
| }, | ||
| ], | ||
| "workflows": [ | ||
| { | ||
| "binding": "DYNAMIC_CALLABLE_WORKFLOWS", | ||
| "name": "kody-runtime-dynamic-callable-workflows", | ||
| "class_name": "DynamicCallableWorkflow", | ||
| }, | ||
| ], | ||
| "durable_objects": { | ||
| "bindings": [ | ||
| // Runtime-owned classes, local to this script. | ||
| { | ||
| "class_name": "StorageRunner", | ||
| "name": "STORAGE_RUNNER", | ||
| }, | ||
| { | ||
| "class_name": "RunLog", | ||
| "name": "RUN_LOG", | ||
| }, | ||
| { | ||
| "class_name": "PackageRealtimeSession", | ||
| "name": "PACKAGE_REALTIME_SESSION", | ||
| }, | ||
| { | ||
| "class_name": "PackageServiceInstance", | ||
| "name": "PACKAGE_SERVICE_INSTANCE", | ||
| }, | ||
| // Main-worker classes reached cross-script. UserMeter stays | ||
| // in the main Worker per ADR 0016; the rest back capability | ||
| // execution paths reachable from package code. | ||
| { | ||
| "class_name": "UserMeter", | ||
| "name": "USER_METER", | ||
| "script_name": "kody", | ||
| }, | ||
| { | ||
| "class_name": "MCP", | ||
| "name": "MCP_OBJECT", | ||
| "script_name": "kody", | ||
| }, | ||
| { | ||
| "class_name": "RemoteConnectorSession", | ||
| "name": "REMOTE_CONNECTOR_SESSION", | ||
| "script_name": "kody", | ||
| }, | ||
| { | ||
| "class_name": "McpClientHub", | ||
| "name": "MCP_CLIENT_HUB", | ||
| "script_name": "kody", | ||
| }, | ||
| { | ||
| "class_name": "JobManager", | ||
| "name": "JOB_MANAGER", | ||
| "script_name": "kody", | ||
| }, | ||
| { | ||
| "class_name": "Mailbox", | ||
| "name": "MAILBOX", | ||
| "script_name": "kody", | ||
| }, | ||
| { | ||
| "class_name": "RepoSession", | ||
| "name": "REPO_SESSION", | ||
| "script_name": "kody", | ||
| }, | ||
| ], | ||
| }, | ||
| "d1_databases": [ | ||
| { | ||
| "binding": "APP_DB", | ||
| "database_name": "kody", | ||
| "migrations_dir": "./../worker/migrations", | ||
| }, | ||
| { | ||
| "binding": "AUDIT_DB", | ||
| "database_name": "kody-audit", | ||
| "migrations_dir": "./../worker/audit-migrations", | ||
| }, | ||
| ], | ||
| "kv_namespaces": [ | ||
| { | ||
| "binding": "OAUTH_KV", | ||
| }, | ||
| { | ||
| "binding": "BUNDLE_ARTIFACTS_KV", | ||
| }, | ||
| ], | ||
| "r2_buckets": [ | ||
| { | ||
| "binding": "COMMUNITY_ASSETS", | ||
| "bucket_name": "kody-community-assets", | ||
| }, | ||
| ], | ||
| "queues": { | ||
| "producers": [ | ||
| { | ||
| "binding": "PLATFORM_FEEDBACK_DISPATCH_QUEUE", | ||
| "queue": "kody-platform-feedback-dispatch", | ||
| }, | ||
| { | ||
| "binding": "COMMUNITY_ACTIVITY_DISPATCH_QUEUE", | ||
| "queue": "kody-community-activity-dispatch", | ||
| }, | ||
| { | ||
| "binding": "SCHEDULED_DISPATCH_QUEUE", | ||
| "queue": "kody-scheduled-dispatch", | ||
| }, | ||
| { | ||
| "binding": "PACKAGE_EVENTS_DISPATCH_QUEUE", | ||
| "queue": "kody-package-events-dispatch", | ||
| }, | ||
| { | ||
| "binding": "WEBHOOK_DISPATCH_QUEUE", | ||
| "queue": "kody-webhook-dispatch", | ||
| }, | ||
| ], | ||
| }, | ||
| "vectorize": [ | ||
| { | ||
| "binding": "CAPABILITY_VECTOR_INDEX", | ||
| "index_name": "kody-capabilities-prod", | ||
| }, | ||
| ], | ||
| "analytics_engine_datasets": [ | ||
| { | ||
| "binding": "USAGE_EVENTS", | ||
| "dataset": "kody_usage_events", | ||
| }, | ||
| { | ||
| "binding": "FLAG_EXPOSURES", | ||
| "dataset": "kody_flag_exposures", | ||
| }, | ||
| { | ||
| "binding": "MCP_PROTOCOL_EVENTS", | ||
| "dataset": "kody_mcp_protocol_events", | ||
| }, | ||
| ], | ||
| "ai": { | ||
| "binding": "AI", | ||
| }, | ||
| "vars": { | ||
| "SENTRY_ENVIRONMENT": "production", | ||
| "SENTRY_TRACES_SAMPLE_RATE": 0, | ||
| "ARTIFACTS_NAMESPACE": "production", | ||
| "PACKAGE_APP_BASE_URL": "https://kodyapps.dev", | ||
| }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Compare required env-schema keys against the runtime worker bindings.
set -euo pipefail
echo "== env schema =="
cat -n packages/worker/src/env-schema.ts
echo "== omitted binding usage in the runtime entry graph =="
rg -nP '\b(ASSETS|EMAIL|EMAIL_BLOBS|EMAIL_EVENTS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH)\b' \
--glob 'packages/worker/src/**/*.ts' --glob '!**/*.test.ts' -C2Repository: kentcdodds/kody
Length of output: 13740
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== schema and validation references =="
rg -n -C3 'EnvSchema|safeParse|parse\(|env-schema|EMAIL_BLOBS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH|ASSETS' \
packages/worker packages/runtime-worker --glob '!**/*.test.*'
echo "== all repository references to omitted bindings =="
rg -n -C2 '\b(ASSETS|EMAIL|EMAIL_BLOBS|EMAIL_EVENTS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH)\b' \
--glob '!**/*.test.*'
echo "== runtime configuration keys =="
cat -n packages/runtime-worker/wrangler.jsonc | sed -n '1,280p'Repository: kentcdodds/kody
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== environment validation implementation =="
cat -n packages/worker/src/app/env.ts
echo "== runtime entrypoints and imports =="
rg -n -C4 'from .*(app/env|`#worker/env`|worker/src)|validateEnv|getEnv|fetch\s*\(' \
packages/runtime-worker packages/worker/src/index.ts packages/worker/src --glob '*.ts' \
--glob '!**/*.test.ts' | head -n 500
echo "== runtime production and preview vars =="
rg -n -C8 '"(production|preview)"|SENTRY_ENVIRONMENT|AUTH_RATE_LIMITER|service' \
packages/runtime-worker/wrangler.jsonc
echo "== focused omitted-binding references in source =="
rg -n -C3 'EMAIL_BLOBS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH|ASSETS' \
packages/worker/src packages/runtime-worker --glob '*.ts' --glob '*.tsx' \
--glob '!**/*.test.*' | head -n 500Repository: kentcdodds/kody
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== runtime-worker files =="
git ls-files packages/runtime-worker
echo "== runtime-worker entry references =="
rg -n -C5 'getEnv|EnvSchema|validateEnv|from .*(worker|`#app`|`#worker`)|export default|WorkerEntrypoint|fetch' \
packages/runtime-worker --glob '!**/*.test.*'
echo "== runtime Worker source files and imports of shared handlers =="
fd -t f . packages/runtime-worker | while read -r file; do
case "$file" in
*.ts|*.tsx|*.js|*.mjs) echo "---- $file"; sed -n '1,220p' "$file" ;;
esac
doneRepository: kentcdodds/kody
Length of output: 1356
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== runtime worker source candidates =="
fd -t f 'runtime-worker|index' packages/worker/src packages/runtime-worker
echo "== runtime worker source references =="
rg -n -C5 'runtime-worker|RuntimeWorker|getEnv|EnvSchema|SENTRY_ENVIRONMENT|AUTH_RATE_LIMITER' \
packages/worker/src --glob '*.ts' --glob '*.tsx' --glob '!**/*.test.*'
echo "== runtime worker source file contents =="
for file in $(fd -t f 'runtime-worker' packages/worker/src); do
echo "---- $file"
cat -n "$file"
doneRepository: kentcdodds/kody
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== runtime-worker.ts =="
cat -n packages/worker/src/runtime-worker.ts
echo "== runtime handlers imported by runtime-worker.ts =="
for file in packages/worker/src/app/handlers/package-app.ts packages/worker/src/app/package-app-origin.ts packages/worker/src/package-invocations/http.ts packages/worker/src/package-invocations/*.ts; do
if [ -f "$file" ]; then
echo "---- $file"
rg -n -C4 'getEnv|handlePackage|EnvSchema|EMAIL_BLOBS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH' "$file"
fi
doneRepository: kentcdodds/kody
Length of output: 5048
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== direct environment-validation calls in runtime request handlers =="
rg -n -C6 'getEnv|createAppRouter|handlePackageApp|handlePackageInvocation|packageAppOrigin' \
packages/worker/src/app/handlers/package-app.ts \
packages/worker/src/app/package-app-origin.ts \
packages/worker/src/package-invocations/http.ts
echo "== imports of getEnv and runtime handler dependency names =="
rg -n '^import .*getEnv|from .*app/env|from .*app/handler|from .*ssr-render|from .*oauth-handlers' \
packages/worker/src/app/handlers/package-app.ts \
packages/worker/src/app/package-app-origin.ts \
packages/worker/src/package-invocations/http.ts \
packages/worker/src/package-invocations --glob '*.ts'
echo "== all runtime config binding declarations, vars, and secrets-related comments =="
cat -n packages/runtime-worker/wrangler.jsonc | sed -n '1,430p'Repository: kentcdodds/kody
Length of output: 19278
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== runtime package invocation attachment path =="
cat -n packages/worker/src/package-invocations/module-execution.ts | sed -n '240,295p'
rg -n -C8 'getEmailAttachmentById|EMAIL_BLOBS' \
packages/worker/src/email packages/worker/src/package-invocations
echo "== package invocation validation and environment access =="
cat -n packages/worker/src/package-invocations/http.ts | sed -n '200,380p'
rg -n -C5 'getEnv|EMAIL_BLOBS|AUTH_RATE_LIMITER|checkAuthRateLimit' \
packages/worker/src/package-invocations --glob '*.ts'
echo "== all runtime-worker reachable references to omitted bindings =="
rg -n -C4 'EMAIL_BLOBS|EMAIL|EMAIL_EVENTS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH|ASSETS' \
packages/worker/src/runtime-worker.ts \
packages/worker/src/app/handlers/package-app.ts \
packages/worker/src/app/package-app-origin.ts \
packages/worker/src/package-invocations \
packages/worker/src/package-runtime \
packages/worker/src/storage-runner.ts \
packages/worker/src/run-records --glob '*.ts'Repository: kentcdodds/kody
Length of output: 50372
Bind EMAIL_BLOBS in both runtime environments. The package-invocation path reads email attachments through env.EMAIL_BLOBS, but the runtime Worker does not define this R2 binding.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/runtime-worker/wrangler.jsonc` around lines 94 - 246, Add an
`EMAIL_BLOBS` R2 bucket binding to the runtime Worker's R2 configuration for
both runtime environments, using the existing email-blob bucket name and binding
conventions. Ensure package-invocation code can access the bucket through
`env.EMAIL_BLOBS` in each environment.
There was a problem hiding this comment.
Valid — fixed in 290f2d0. The runtime worker's env-schema requires EMAIL_BLOBS (package capability paths can read email blobs), so packages/runtime-worker/wrangler.jsonc now binds it in both environments: kody-email-blobs (production) and kody-preview-email-blobs (preview), and the config generator test fixture covers the copy. The other omitted bindings (ASSETS, AUTH_RATE_LIMITER, OAUTH_PURGE_COORDINATOR, STRIPE_PLAN_REFRESH, etc.) are main-lane only and stay off the runtime worker.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| STORAGE_RUNNER: DurableObjectNamespace /* StorageRunner from kody-runtime */; | ||
| RUN_LOG: DurableObjectNamespace /* RunLog from kody-runtime */; | ||
| USER_METER: DurableObjectNamespace<import("./src/index").UserMeter>; | ||
| MAILBOX: DurableObjectNamespace<import("./src/index").Mailbox>; | ||
| STRIPE_PLAN_REFRESH: DurableObjectNamespace<import("./src/index").StripePlanRefresh>; | ||
| REPO_SESSION: DurableObjectNamespace<import("./src/index").RepoSession>; | ||
| PACKAGE_REALTIME_SESSION: DurableObjectNamespace<import("./src/index").PackageRealtimeSession>; | ||
| PACKAGE_SERVICE_INSTANCE: DurableObjectNamespace<import("./src/index").PackageServiceInstance>; | ||
| DYNAMIC_CALLABLE_WORKFLOWS: Workflow<Parameters<import("./src/index").DynamicCallableWorkflow['run']>[0]['payload']>; | ||
| PACKAGE_REALTIME_SESSION: DurableObjectNamespace /* PackageRealtimeSession from kody-runtime */; | ||
| PACKAGE_SERVICE_INSTANCE: DurableObjectNamespace /* PackageServiceInstance from kody-runtime */; | ||
| RUNTIME_WORKER: Fetcher /* kody-runtime */; | ||
| DYNAMIC_CALLABLE_WORKFLOWS: Workflow /* DynamicCallableWorkflow from kody-runtime */; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find call sites of the runtime-owned Durable Object and Workflow bindings.
set -euo pipefail
rg -nP -C4 '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' \
--glob '*.ts' --glob '!**/*.d.ts'Repository: kentcdodds/kody
Length of output: 153
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- binding references ---'
rg -n -C3 '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' . \
--glob '!**/node_modules/**' \
--glob '!**/.git/**'
printf '%s\n' '--- worker source files ---'
git ls-files 'packages/worker/*' 'packages/worker/**/*' | sed -n '1,160p'
printf '%s\n' '--- DurableObjectNamespace and Workflow declarations/usages ---'
rg -n -C2 'DurableObjectNamespace|DYNAMIC_CALLABLE_WORKFLOWS|Workflow<' packages/worker packages \
--glob '!**/node_modules/**' \
--glob '!**/*.d.ts' | sed -n '1,240p'Repository: kentcdodds/kody
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- production call sites in packages/worker/src ---'
rg -n -C2 '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' \
packages/worker/src \
--glob '*.ts' \
--glob '!**/*.test.ts' \
--glob '!**/*.spec.ts' \
--glob '!**/test/**' \
| sed -n '1,360p'
printf '%s\n' '--- type assertions and shared interfaces around these bindings ---'
rg -n -C3 'as unknown as|Rpc|DurableObjectNamespace|Workflow' packages/worker/src \
--glob '*.ts' \
--glob '!**/*.test.ts' \
--glob '!**/*.spec.ts' \
| rg -n -C2 'STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS|Rpc|DurableObjectNamespace|Workflow' \
| sed -n '1,360p'Repository: kentcdodds/kody
Length of output: 44367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- storage runner client ---'
sed -n '1,45p;560,620p' packages/worker/src/storage-runner.ts
printf '%s\n' '--- run log client and RPC type ---'
sed -n '190,255p' packages/worker/src/run-records/service.ts
rg -n 'export (type|interface) RunLogRpc|type RunLogRpc|interface RunLogRpc|runLogRpc\(' packages/worker/src/run-records/service.ts
printf '%s\n' '--- package service client ---'
sed -n '170,245p' packages/worker/src/package-runtime/package-service.ts
printf '%s\n' '--- realtime session client ---'
sed -n '845,925p' packages/worker/src/package-runtime/realtime-session.ts
printf '%s\n' '--- workflow binding resolver and workflow calls ---'
sed -n '135,180p;900,970p;1080,1135p;1260,1325p' packages/worker/src/package-runtime/package-workflows.tsRepository: kentcdodds/kody
Length of output: 19119
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- RunLogRpc declaration and imports ---'
sed -n '1,35p' packages/worker/src/run-records/service.ts
rg -n -C3 'getPackageServiceStub|\.fetch\(|PackageService' packages/worker/src/package-runtime/package-service.ts \
| sed -n '1,240p'
printf '%s\n' '--- all production calls on package-service and realtime stubs ---'
python3 - <<'PY'
from pathlib import Path
for name in [
"packages/worker/src/package-runtime/package-service.ts",
"packages/worker/src/package-runtime/realtime-session.ts",
]:
text = Path(name).read_text().splitlines()
print(f"--- {name} ---")
for i, line in enumerate(text, 1):
if "getPackageServiceStub" in line or "getPackageRealtimeStub" in line:
lo, hi = max(1, i - 8), min(len(text), i + 35)
for n in range(lo, hi + 1):
print(f"{n}:{text[n-1]}")
PY
printf '%s\n' '--- direct methods on runtime-owned bindings in production source ---'
rg -n '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' packages/worker/src \
--glob '*.ts' \
--glob '!**/*.test.ts' \
--glob '!**/*.spec.ts' \
| rg '\.(get|fetch|idFromName|create)\b|\.get\(' \
| sed -n '1,240p'Repository: kentcdodds/kody
Length of output: 14082
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- local RunLog RPC contract and implementation ---'
rg -n -C4 'RunLogRpc|class RunLog|RunLogBase|export .*RunLog' \
packages/worker/src/run-records/run-log-do.ts \
packages/worker/src/run-records \
| sed -n '1,260p'
printf '%s\n' '--- runtime package definitions for corresponding RPC contracts ---'
rg -n -C3 'StorageRunner|RunLog|PackageRealtimeSession|PackageServiceInstance|DynamicCallableWorkflow|Rpc' \
packages/runtime-worker packages \
--glob '!**/*.test.ts' \
--glob '!**/*.spec.ts' \
--glob '!**/worker-configuration.d.ts' \
| sed -n '1,320p'
printf '%s\n' '--- workflow instance helper declarations and calls ---'
rg -n -C3 'getExistingWorkflowInstance|readWorkflowInstanceSummary|workflowBinding\.(get|create)|WorkflowInstance' \
packages/worker/src/package-runtime/package-workflows.ts \
| sed -n '1,280p'Repository: kentcdodds/kody
Length of output: 38831
Preserve explicit RPC contracts for runtime-owned stubs. PACKAGE_REALTIME_SESSION and PACKAGE_SERVICE_INSTANCE use fetch(), and DYNAMIC_CALLABLE_WORKFLOWS restores its payload type. However, STORAGE_RUNNER and RUN_LOG call RPC methods through local as unknown as contracts. Move these contracts to a shared runtime interface so RPC signature changes remain type-checked.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/worker/worker-configuration.d.ts` around lines 58 - 67, Add the
explicit RPC contracts currently represented by local as unknown as casts for
STORAGE_RUNNER and RUN_LOG to a shared runtime interface, then type both
DurableObjectNamespace declarations with that interface. Remove the local casts
and ensure the shared interface exposes all RPC methods and payload types so
signature changes remain type-checked.
There was a problem hiding this comment.
No change needed here. worker-configuration.d.ts is generated by wrangler types — for cross-script DO/Workflow bindings it cannot emit the class generic, so the untyped DurableObjectNamespace /* ... from kody-runtime */ shape is wrangler's output, not hand-written. The call sites are unchanged by this PR: both workers compile from the same source tree, and the existing local RPC contracts (RunLogRpc in run-records/service.ts, the typed shape in storage-runner.ts) still describe exactly what the classes implement. Promoting those to shared interfaces in packages/shared is a reasonable follow-up, but this PR keeps the cross-worker surface limited to the coarse contracts ADR 0016 calls for (packages/shared/src/runtime-worker.ts), so I'm leaving the pre-existing internal typing as is.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
| // Transfer migrations move Durable Object storage between scripts; | ||
| // changing or removing one after it ships would orphan or duplicate | ||
| // storage, so every transferred_classes migration must exactly match a | ||
| // reviewed baseline entry. | ||
| const protectedTransferMigrations = | ||
| baseline.protected_transfer_migrations ?? [] | ||
| for (const protectedTransfer of protectedTransferMigrations) { | ||
| const current = migrationsByTag.get(protectedTransfer.tag) | ||
| const currentTransfers = normalizeTransferredClasses( | ||
| current?.transferred_classes, | ||
| ) | ||
| if ( | ||
| !currentTransfers || | ||
| !sameStrings( | ||
| sortedTransferKeys(currentTransfers), | ||
| sortedTransferKeys(protectedTransfer.transferred_classes), | ||
| ) | ||
| ) { | ||
| errors.push( | ||
| `${configPath}: protected transferred_classes migration "${protectedTransfer.tag}" was removed, renamed, or changed (expected ${protectedTransfer.transferred_classes.map((transfer) => `${transfer.from_script}/${transfer.from} -> ${transfer.to}`).join(', ')}).`, | ||
| ) | ||
| } | ||
| } | ||
| const protectedTransferTags = new Set( | ||
| protectedTransferMigrations.map(({ tag }) => tag), | ||
| ) | ||
| for (const migration of migrations) { | ||
| if ( | ||
| migration.transferred_classes !== undefined && | ||
| typeof migration.tag === 'string' && | ||
| !protectedTransferTags.has(migration.tag) | ||
| ) { | ||
| errors.push( | ||
| `${configPath}: transferred_classes migration "${migration.tag}" is not recorded in ${defaultDurableObjectBaselinePath}. Update the reviewed baseline so this migration remains protected after it lands.`, | ||
| ) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
The guardrail reads only top-level migrations, so the runtime Worker's per-environment migration is unprotected. packages/runtime-worker/wrangler.jsonc declares env.preview.migrations with new_sqlite_classes for the four runtime classes. checkDurableObjectConfig collects migrations only from config.migrations, so that list is neither matched against the baseline nor reported as unrecorded.
tools/check-deploy-guardrails.ts#L265-L302: collect migrations from eachenv.<name>section in addition to the top level, and scope the duplicate-tag check per section so an environment may reuse the tagv1.tools/ci/durable-object-baseline.json#L165-L241: after the collector change, record the previewv1new_sqlite_classesmigration forpackages/runtime-worker/wrangler.jsoncso it stays protected.
📍 Affects 2 files
tools/check-deploy-guardrails.ts#L265-L302(this comment)tools/ci/durable-object-baseline.json#L165-L241
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tools/check-deploy-guardrails.ts` around lines 265 - 302, Update
checkDurableObjectConfig and its migration collection to include migrations from
every config.env.<name> section, while keeping duplicate-tag validation scoped
per section so environment-specific tags such as v1 may be reused. Add the
preview v1 new_sqlite_classes migration for
packages/runtime-worker/wrangler.jsonc to tools/ci/durable-object-baseline.json,
ensuring it is protected by the baseline.
There was a problem hiding this comment.
Fixed in 290f2d0: checkDurableObjectConfig now collects migrations from every env.<name>.migrations section in addition to the top level, with the duplicate-tag check scoped per section (so preview may reuse v1). Error messages include the section location, baseline entries carry an optional location field, and the runtime worker's preview v1 new_sqlite_classes migration is now recorded in tools/ci/durable-object-baseline.json ("location": "env.preview.migrations"). Covered by a new test in tools/check-deploy-guardrails.node.test.ts.
There was a problem hiding this comment.
Skipped: comment is from another GitHub bot.
…althcheck guards, env-scoped migration guardrails, runbook completeness Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…inned names, no ai binding, injected vars) Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…tion Resolves overlap between the runtime lane (ADR 0016 track b) and the jobs lane extraction that landed on main: - runtime worker reaches jobs via the JOBS service binding (JobManager moved to kody-jobs) instead of a cross-script binding - runtime config generator copies the resolved JOBS service name from the main generated config - production runtime deploy pins --name kody-runtime and rewrites cross-script refs to the deployed kody-production script name - baseline records main env.preview/env.test migration sections and the jobs worker migrations for the extended guardrails Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
… classes A preview app script deployed before a Durable Object class moved to another worker (e.g. JobManager -> kody-jobs) still has live DO namespaces for that class, so the API rejects new versions with 'does not export class' [code: 10064]. Preview scripts are disposable (data lives in per-preview D1/KV/R2), so the bootstrap deploy now deletes the stale script and deploys fresh when it hits that error. Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…moved DO classes" This reverts commit 1d93dc9.
…er extraction The wildcard zone route for per-user package-app subdomains (*.kodyapps.dev/*, decision 0017) is published by the runtime worker alongside the apex custom domain, since the package-app host belongs to the runtime lane (ADR 0016). The main worker's generated config keeps publishing only the app origin and legacy origins. Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
Local dev (and any fresh script) replays the whole top-level migrations chain, which still created JobManager even though the class moved to the jobs worker (ADR 0016) and is no longer exported by the main script, so 'npm run dev' failed to boot with 'Class extends value undefined'. The preview and test envs already got chains without the v6/v9 pair; the top-level (inherited by production) chain now matches. The deployed production script is past these tags, so its migration diff is empty. Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…t pitfall Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
🧪 Runtime testing: multi-worker local devVerdict: Boot failure found on HEAD (also reproduces on origin/main) — fixed in
|
Intent
Implement track (b) of ADR 0016: move the package runtime lane — the fastest-churning, riskiest untrusted-code execution subsystem — out of the mono
kodyWorker into an independently deployedkody-runtimeWorker, so a bad runtime deploy can no longer take down login/MCP and rollback becomes per-subsystem.Summary
Runtime worker (
packages/runtime-worker/)kody-runtimewith entrypackages/worker/src/runtime-worker.ts(kept in the shared worker source tree so#worker/#appimport maps and pre-bundledsrc/generated/modules resolve identically). It owns: the package-app origin (PACKAGE_APP_BASE_URL/kodyapps.devcustom domain), inline package-app serving, the package invocation API,DynamicCallableWorkflow, and exportsStorageRunner,RunLog,PackageRealtimeSession,PackageServiceInstanceplus the loopback entrypoints (PackageAppRuntimeBridge,KodyFetchGateway).LOADER,APP_LOADER) and directAPP_DB/AUDIT_DB/KV/R2/queue/Vectorize/AI bindings move with it — no relational access over RPC.transferred_classesscript migration (from_script: "kody"), applied on the first production deploy:{ "tag": "v1", "transferred_classes": [{ "from": "StorageRunner", "from_script": "kody", "to": "StorageRunner" }, ...RunLog/PackageRealtimeSession/PackageServiceInstance] }UserMeterstays in main; the runtime worker reaches it (and MCP/connector/job/mailbox/repo DOs used by capability execution) via cross-script DO bindings (script_name: "kody").Main worker
RUNTIME_WORKERservice binding (runtime-worker-routing.ts); without the binding (tests,--env test) it keeps serving in-process, so the test suite and single-worker flows are unchanged.script_name: "kody-runtime") for dashboard/account-export reads. Coarse-grained shared contract (healthcheck) inpackages/shared/src/runtime-worker.ts.CI/CD
tools/ci/runtime-worker-config.tsderives deployable runtime configs from the provisioned main config (shared data-plane ids, per-PR worker names, package-app route ownership) and, for preview, a bootstrap main config that breaks the fresh-pair circular binding reference. Deploy order: preview = main(bootstrap) → runtime → main(full); production = runtime (applies the transfer) → main. Both workers get secrets sync and/health+/__runtime/healthhealthchecks.tools/check-deploy-guardrails.tsnow protectstransferred_classesmigrations: any transfer must exactly match a reviewed baseline entry.Local dev
npm run dev/npm run previewrun both scripts in one Miniflare via a secondary--config(wrangler multi-config), so the service binding and cross-script DOs resolve locally.Runbook: docs/contributing/architecture/runtime-worker-migration-runbook.md — migration mechanics, coordinated deploy order, workflow-cutover caveat, rollback. Production migrations are NOT executed by this PR's session; the runbook is executed by the parent session after preview verification.
Testing
npm run validatepasses (598 test files / 2028 tests, e2e, mcp, typecheck, lint, format, guardrails, migrations, docs, runtime dry-run build).tools/ci/runtime-worker-config.node.test.ts(name rewriting, resource-id copying, bootstrap generation, production route/migration) and transfer-migration guardrail cases intools/check-deploy-guardrails.node.test.ts.npm run runtime:build(wranglerdeploy --dry-runof the runtime config) verifies the runtime bundle and production bindings.transferred_classesmigration (preview pairs are created fresh withnew_sqlite_classes); see runbook.System changes
kody-runtime;kodyapps.devcustom domain moves fromkodytokody-runtime.StorageRunner/RunLog/PackageRealtimeSession/PackageServiceInstanceon first production deploy (guardrail-protected).DynamicCallableWorkflowbecomes a new workflow on the runtime script; in-flight instances at cutover are orphaned (see runbook caveat).Link to Devin session: https://app.devin.ai/sessions/341d72934bdf4541a3877f07b6c8be93
Requested by: @kentcdodds
Summary by CodeRabbit
New Features
Bug Fixes
Documentation