wrangler 4.x errors when it finds both a local wrangler.json and a
parent .wrangler/deploy/config.json redirect file — "base paths do not
match" — which is exactly what happens if you cd into dist/zpan after
the @cloudflare/vite-plugin build.
The old `cd dist/zpan` workaround predates the vite-plugin generating
.wrangler/deploy/config.json on build. Now that the redirect file
exists, running wrangler from the repo root is the correct path:
wrangler picks up the redirect, uses dist/zpan/wrangler.json, and
resolves ../client / index.js / etc. relative to *that* config's
location — all paths correct.
Empirically: `wrangler deploy --dry-run` from root reports
"Read 333 files from the assets directory .../dist/client" and all
three bindings (D1 + R2 + ASSETS) are recognized. No 404.
Fixes the cloudflare deploy job in bonaysoft/zpan's first run of the
new dispatcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, push to master triggered 6 separate deploy workflows (CF +
5 new v2.5.0 targets). If the fork hadn't configured, say, AWS or Azure
secrets, those workflows would run just to fail on "Check required
secrets" — producing 5 red X's in Actions tab per push, 5 failure
notifications, 5 wasted runner allocations.
Collapse to one top-level `deploy.yml` dispatcher that:
1. Runs a lightweight `detect` job (~5s) probing which platform secret
bundles are fully present — without invoking secrets.* in job-level
`if:` (which GH disallows).
2. Invokes the corresponding reusable child workflow via `uses:` +
`secrets: inherit` only when that platform's flag is true.
Each child workflow (`deploy-<target>.yml`) is now a reusable workflow:
- `push: [master]` trigger → removed (dispatcher owns push)
- `workflow_call:` trigger → added (invoked by dispatcher)
- `workflow_dispatch:` trigger → kept (manual runs via Actions UI)
- `if: github.repository != 'saltbo/zpan'` job guard → removed
(dispatcher enforces this once)
The old `deploy.yml` (CF Workers flow) is renamed to
`deploy-cloudflare.yml` for consistency with the other 5. Content of
the CF flow is unchanged.
For a fork with only CF configured: 1 dispatcher run + 1 cloudflare run.
For a fork with nothing configured: 1 dispatcher run with all 6 child
jobs shown as "Skipped" (not failed), and a ::notice:: pointing at the
README secrets table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\`wrangler r2 bucket list\` has no \`--json\` flag (confirmed via
\`--help\`), so piping its text output to jq would fail silently. Switch
the \"Ensure R2 public-images bucket exists\" step to the CF REST API
which returns proper JSON and also lets us check-then-create in two
clean HTTP calls. Both endpoints use the same \`CLOUDFLARE_API_TOKEN\`
with R2 Storage: Edit scope.
Idempotency unchanged: check-exists-before-create.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new workflow steps turn the CF deploy into fully zero-touch for
the avatar/logo feature:
1. Ensure R2 bucket exists — `wrangler r2 bucket list --json` → create if
missing. Idempotent; prior deploys skip creation.
2. Enable managed public URL — `PUT /r2/buckets/.../domains/managed`
with {enabled:true}. CF returns the same pub-<hash>.r2.dev on every
call once enabled, so this is idempotent too. Then GET the domain
from the same endpoint and capture into step output.
3. Upsert `PUBLIC_IMAGES_URL` as a Worker secret via
`wrangler secret put`. Always overwritten to keep in sync with the
managed domain (which is stable but this is defensive).
Order: bucket/URL steps run BEFORE `wrangler deploy` so the binding
declared in wrangler.toml (new `[[r2_buckets]] binding = "PUBLIC_IMAGES"`)
references an existing bucket. The secret step runs AFTER deploy —
wrangler secret put requires the Worker to exist.
`wrangler.toml`: new `[[r2_buckets]]` for production and
`[[env.staging.r2_buckets]]` for the staging environment. Staging uses
a distinct bucket (`zpan-public-images-staging`) so preview deploys
don't mix objects into production.
README updated: `CLOUDFLARE_API_TOKEN` now requires
**R2 Storage: Edit** in addition to Workers Scripts + D1. The workflow
surfaces an actionable error message when this scope is missing.
User experience on a fresh fork: add the 2 GitHub secrets (ACCOUNT_ID
+ API_TOKEN with the 3 scopes), push. Workflow creates D1, creates R2,
enables public URL, migrates, builds, deploys, sets both BETTER_AUTH_SECRET
(random) and PUBLIC_IMAGES_URL (from R2). Avatars + org logos work
immediately — no trip to Admin → Storages required.
Non-CF deployments continue to require a user-added mode='public'
storage (no change to those paths).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Running `wrangler deploy --assets dist` from the repo root used the
source wrangler.toml (no assets.directory) and treated the whole dist/
tree as static assets, so the built worker under dist/zpan/ was served
as a file and SPA routes 404'd. It also forced wrangler to re-bundle
the TS entry, diverging from what @cloudflare/vite-plugin produced.
The vite plugin emits dist/zpan/wrangler.json with the right asset dir
(../client), the prebuilt worker, and all bindings. Deploy from there
so the GitHub Actions workflow matches Cloudflare Workers Builds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wrangler d1 create has no --json flag, so the first-time deploy path
errored with "Unknown argument: json" on fresh forks. d1 list --json
masked the bug for anyone whose CF account already had zpan-db.
Parse the TOML snippet wrangler prints instead, and fail fast if the
UUID can't be extracted so downstream sed/wrangler steps don't run with
an empty database_id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add Azure Functions deployment target (v4, Node 22)
- server/entry-azure.ts: Azure Functions v4 handler wrapping the Hono
app via app.http(); uses createLibsqlPlatform for Turso and serves
the SPA from ./dist via @hono/node-server/serve-static
- server/azure-host.json: runtime manifest (extensionBundle v4)
- deploy/azure-functions/main.bicep: idempotent Bicep template
provisioning Storage Account, Consumption plan and Function App;
BETTER_AUTH_SECRET handled separately by the workflow
- .github/workflows/deploy-azure.yml: 8-step workflow (secret check,
checkout, Node setup, az login, Bicep deploy, build, db:migrate,
func publish) with BETTER_AUTH_SECRET generate-if-missing logic
- package.json: build:azure script + @azure/functions dependency
- docs/deploy/azure-functions.md: setup guide covering SP JSON format,
required secrets, and local emulation with func start
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: address review issues in Azure Functions deploy
- Move BETTER_AUTH_SECRET and APP_URL setup to before func publish
(bootstrap.ts throws on missing secret; any request between publish
and the old secret-set step would have returned 500)
- Remove placeholder appUrl Bicep param; workflow sets APP_URL and
BETTER_AUTH_URL via appsettings after Bicep, before publish
- Fix HttpRequest→Request body handling: construct a proper Web API
Request with body cast and duplex option instead of double-casting
HttpRequest, ensuring POST/PUT/PATCH body-reading routes work
- Add push: branches: [master] trigger + upstream guard to match other
deploy workflow conventions; document the auto-deploy behaviour
- Update docs/deploy/azure-functions.md to reflect the push trigger
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* ci: re-trigger CI for review fixes
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
* feat: v2.5.0 T6 — Google Cloud Run deployment (service.yaml + workflow + docs)
Add Cloud Run as a first-class deploy target. Reuses the existing root
Dockerfile via gcloud run deploy --source (Cloud Build). Turso for DB,
external S3-compatible storage. Follows the standard 8-step workflow
contract: secret check, release resolution, GCloud auth, Turso migration,
Secret Manager upsert, deploy. min-instances=0 for free-tier eligibility
with cold-start callout in docs.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: resolve first-deploy failure and drive deploy from service.yaml
BLOCKER: BETTER_AUTH_URL and TURSO_AUTH_TOKEN were passed to --set-secrets
even when the corresponding Secret Manager entries didn't exist yet.
BETTER_AUTH_URL isn't known until after the first deploy (it IS the Cloud
Run service URL). TURSO_AUTH_TOKEN is optional. Both caused 'secret not
found' aborts.
Fix: separate deploy into two phases.
Phase 1 — gcloud run services replace with service.yaml, which only
references the guaranteed secrets (turso-database-url, better-auth-secret).
Phase 2 — post-deploy step captures the service URL, upserts better-auth-url
and (if provided) turso-auth-token in Secret Manager, then wires them into
the running service via gcloud run services update --update-secrets.
MINOR: service.yaml was orphaned — the workflow used gcloud run deploy
--source . with inline flags instead. Rebuilt workflow to use gcloud builds
submit to build the image, then gcloud run services replace to drive the
deploy from the manifest. PROJECT_ID is substituted at deploy time.
Also demote BETTER_AUTH_URL from required to optional GitHub secret
(auto-derived from Cloud Run service URL on first deploy) and update docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: v2.5.0 T4 — Netlify deployment target
- server/entry-netlify.ts: Netlify Functions v2 (ESM) handler using hono/netlify
adapter; connects to Turso via @libsql/client; skips in-process migrations
(workflow applies them before deploy via drizzle-kit)
- deploy/netlify/netlify.toml: build command, functions directory, SPA fallback redirect
- .github/workflows/deploy-netlify.yml: 8-step workflow — secret guard, tag resolve,
Turso migrations, build, netlify deploy --prod, BETTER_AUTH_SECRET first-deploy, summary
- package.json: add build:netlify script (tsup ESM → netlify/functions)
- docs/deploy/netlify.md: 5-section setup guide covering Turso, site creation,
secrets, deploy trigger, first-boot storage setup, and cost breakdown
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: address Netlify deploy review blockers
BLOCKER 1 — move BETTER_AUTH_SECRET step before Deploy in workflow so
the function always has the secret set before its first cold start.
BLOCKER 2 — replace inline platform construction in entry-netlify.ts
with createLibsqlPlatform(); removes duplicated db/schema wiring and
re-unifies with the shared factory. migrate() runs at cold start and
is idempotent (~50–100ms) per the workflow's prior drizzle-kit migrate.
BLOCKER 3 — add --external @libsql/client to build:netlify so tsup
leaves the native-binding package for Netlify to resolve; switch
netlify.toml to node_bundler=esbuild so Netlify bundles @libsql/client
from node_modules. Add included_files=["migrations/**"] so the
migrations folder is available in the function zip for migrate().
Minor — replace 2>/dev/null with 2>&1 in deploy step so netlify-cli
errors surface in CI logs instead of being silently swallowed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add Vercel deployment target (Node runtime + Turso)
Adds first-class Vercel support: server/entry-vercel.ts using hono/vercel
handler, deploy/vercel/vercel.json with nodejs22.x function config and SPA
rewrites, build:vercel npm script producing api/entry-vercel.js + dist/,
deploy-vercel GitHub Actions workflow (8-step: secrets check, tag resolve,
checkout, install, migrate, build, link, deploy), and docs/deploy/vercel.md
documenting secrets, quick-start, local dev, and pricing notes.
Edge runtime is explicitly not used — @aws-sdk/client-s3 requires Node APIs.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: auto-generate BETTER_AUTH_SECRET on first Vercel deploy
Remove BETTER_AUTH_SECRET from the required secrets check. Add a
dedicated step that detects whether the secret already exists in the
Vercel project env via `vercel env ls production`, then either upserts
the user-supplied GitHub secret, auto-generates one with openssl on
first deploy, or skips if already present. Auto-generation case appends
a backup warning to GITHUB_STEP_SUMMARY. Docs move BETTER_AUTH_SECRET
to Optional Secrets with a note about the auto-gen behaviour.
Matches the existing CF Workers deploy.yml pattern (step 8 contract).
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- Add deploy.yml: auto-deploys latest upstream release tag on fork sync
- Support manual trigger with optional version override
- Auto-create D1 database, apply migrations, and set BETTER_AUTH_SECRET
- Only runs on forks (skipped on saltbo/zpan)
- Validate required secrets with clear error message
- Update README with fork + GitHub Actions deploy instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
node:24-slim no longer provides an arm/v7 manifest, causing the
release Docker Image job to fail with 'no match for platform'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename 19 integration test files from *.test.ts to *.integration.test.ts.
Configure vitest projects to run them independently with separate coverage
thresholds. CI now reports unit and integration coverage as separate flags
to Codecov.
Unit tests: pure function calls, mocked dependencies, no DB
Integration tests: createTestApp() with in-memory DB + HTTP requests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add --coverage flag to npm test in CI so coverage files are generated.
Fix upload path to coverage-final.json (v8 provider default). Simplify
vitest reporter config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add json/json-summary reporters to vitest config, upload coverage
to Codecov after test step in CI, and add coverage badge to README.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Workers Builds preview versions share the same Worker's bindings.
[env.staging] creates a separate Worker which is not how preview URLs
work. Use preview_database_id instead — preview versions automatically
use the staging D1 database while production uses the main one.
Also cache node_modules in CI to speed up npm ci across jobs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cloudflare vite plugin uses a local Miniflare D1 database which
starts empty. Migrations must be applied before tests can run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split E2E into two parallel jobs:
- e2e-node: Node backend + Vite (--mode node), uses process.env
- e2e-cf: CF Workers via cloudflare vite plugin, uses .dev.vars
Playwright config switches webServer based on E2E_RUNTIME env var.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
requarks/changelog-action requires explicit tag when it cannot
auto-detect from context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Since b73d7a0 added auto-migrate on Node platform boot, the explicit
drizzle-kit push step conflicts — push creates tables without recording
a migration journal, so the subsequent migrate() call fails with
"table already exists". Also add BETTER_AUTH_SECRET env var required
since ff1ad78 added fail-fast on missing secret.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace pnpm with npm in GitHub Actions workflow
- Upgrade Node.js from 20 to 24 in CI, Dockerfile, and engines
- Pin Node 24.14.1 via volta
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wrangler.toml references assets.directory=./dist which doesn't
exist until frontend is built. CF tests don't need static assets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>