Repository navigation
Extract package runtime lane into kody-runtime worker (ADR 0016) #1384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
59d15ef
b9ed912
3403e58
990b7ad
290f2d0
9ed0ea4
c549ff5
1d93dc9
ebc0c49
fa4eb5e
0ff96f4
4f77afa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,7 +109,7 @@ jobs: | |
| deploy: | ||
| runs-on: ubuntu-latest | ||
| name: 🚀 Deploy to production | ||
| timeout-minutes: 8 | ||
| timeout-minutes: 12 | ||
| needs: sha-guard | ||
| if: needs.sha-guard.outputs.should_deploy == 'true' | ||
| environment: | ||
|
|
@@ -153,6 +153,20 @@ jobs: | |
| set -euo pipefail | ||
| node tools/ci/production-resources.ts ensure --out-config packages/worker/wrangler-production.generated.json | tee -a "$GITHUB_OUTPUT" | ||
|
|
||
| # Package runtime worker (ADR 0016). Shares the main worker's data | ||
| # plane, so it derives resource ids from the generated main config and | ||
| # patches the main config's cross-script references in place. | ||
| - name: 🧩 Generate runtime worker config | ||
| id: runtime_config | ||
| run: | | ||
| set -euo pipefail | ||
| 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 | tee -a "$GITHUB_OUTPUT" | ||
|
|
||
| - name: 🔐 Sync Cloudflare Secrets (bulk) | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
|
|
@@ -185,6 +199,39 @@ jobs: | |
| --set-from-env-optional STRIPE_SECRET_KEY --set-from-env-optional | ||
| STRIPE_WEBHOOK_SECRET | ||
|
|
||
| - 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 | ||
|
|
||
| - name: 🗄️ Apply D1 Migrations | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
|
|
@@ -193,6 +240,27 @@ jobs: | |
| node ./wrangler-env.ts d1 migrations apply APP_DB --remote --config "$WRANGLER_CONFIG" | ||
| node ./wrangler-env.ts d1 migrations apply AUDIT_DB --remote --config "$WRANGLER_CONFIG" | ||
|
|
||
| # 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ 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:
💡 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:
🏁 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 The runbook and workflow do not remove the domain from 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid — fixed in the runbook (290f2d0). Cloudflare refuses to attach a custom domain that another Worker still owns, so the first production There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| 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 | ||
|
|
||
|
Comment on lines
+370
to
+432
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift The runtime deploy lacks the retry and ANSI handling that the main deploy has. Three defects in this step:
🐛 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 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - name: ☁️ Deploy to Cloudflare Workers | ||
| id: deploy | ||
| env: | ||
|
|
@@ -309,6 +377,43 @@ jobs: | |
| fi | ||
| exit 1 | ||
|
|
||
| - 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 | ||
|
|
||
|
Comment on lines
+549
to
+590
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 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 📍 Affects 2 files
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 290f2d0: both runtime healthchecks (deploy.yml production and preview.yml) now fail fast with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - name: 🧪 Execute smoke check (production) | ||
| shell: bash | ||
| env: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 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-runtimeand to${APP_WORKER_NAME}-runtime, although the runtime handler inpackages/worker/src/runtime-worker.tsonly 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, andCAPABILITY_REINDEX_SECRETno longer reachkody-runtime(removed from both theenvblock and the command). Preview intentionally keeps the dotenv-based sync (see the new comment in preview.yml): those files contain only.env.exampleplaceholders 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.