-
Notifications
You must be signed in to change notification settings - Fork 0
Speed up Erxes executor provisioning #3
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
Open
darjss
wants to merge
10
commits into
main
Choose a base branch
from
perf/erxes-provision-fast-path
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
c4601ab
feat(host-cloudflare): add Cloudflare OS user bridge
darjss c8dcf40
chore(host-cloudflare): document request parse boundary
darjss efb7cba
chore(host-cloudflare): configure darjs deployment
darjss 656e908
chore(host-cloudflare): configure Access
darjss 851daa7
fix(graphql): expose nested result shapes
darjss a35045e
fix(execution): teach agents GraphQL selections
darjss dbccd2f
chore(host-cloudflare): align Erxes tenant
darjss 6bddd55
fix(host-mcp): declare execute and resume as read-only for OS action …
darjss 8963d5c
fix(graphql): return list rows by default and resolve unique short names
darjss a3c6a2a
Speed up Erxes executor provisioning on Cloudflare.
darjss File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@executor-js/plugin-graphql": patch | ||
| "@executor-js/execution": patch | ||
| --- | ||
|
|
||
| Expose bounded GraphQL return shapes and teach agents to request nested rows with explicit `select` clauses. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,92 +1,129 @@ | ||
| #!/usr/bin/env bash | ||
| # One-shot deploy for the Executor Cloudflare host. | ||
| # Deploy one Executor Cloudflare host for a Cloudflare OS instance. | ||
| # | ||
| # Provisions everything a fresh account needs and deploys the Worker: | ||
| # 1. verifies wrangler is logged in | ||
| # 2. creates (or reuses) the `executor` D1 database and writes its id into | ||
| # wrangler.jsonc | ||
| # 3. generates + uploads EXECUTOR_SECRET_KEY (the at-rest secret key) if unset | ||
| # 4. deploys the Worker | ||
| # 5. prints the single manual step: configure the Cloudflare Access application | ||
| # The default is the internal erxes demo: | ||
| # worker: erxes-os-internal-executor | ||
| # domain: executor.os.erxes.io | ||
| # D1: erxes-os-internal-executor | ||
| # R2: erxes-os-internal-executor-blobs | ||
| # | ||
| # Idempotent — safe to re-run. Run from anywhere: | ||
| # bash apps/host-cloudflare/scripts/deploy.sh | ||
| # Set INSTANCE_SLUG and EXECUTOR_DOMAIN for another tenant. Every resource name | ||
| # follows INSTANCE_SLUG so multiple installations can share one CF account. | ||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| CONFIG="$APP_DIR/wrangler.jsonc" | ||
| SOURCE_CONFIG="$APP_DIR/wrangler.jsonc" | ||
| CONFIG="$APP_DIR/wrangler.instance.jsonc" | ||
| SECRETS_FILE="${EXECUTOR_SECRETS_FILE:-$APP_DIR/deploy-secrets.json}" | ||
| cd "$APP_DIR" | ||
|
|
||
| EXPECTED_ACCOUNT_ID="7c8392aff8ac4518aa06dfa4b6337ef2" | ||
| INSTANCE_SLUG="${INSTANCE_SLUG:-erxes-os-internal}" | ||
| EXECUTOR_DOMAIN="${EXECUTOR_DOMAIN:-executor.os.erxes.io}" | ||
| WORKER_NAME="${INSTANCE_SLUG}-executor" | ||
| DATABASE_NAME="$WORKER_NAME" | ||
| BUCKET_NAME="${WORKER_NAME}-blobs" | ||
|
|
||
| step() { printf '\n\033[1;36m==> %s\033[0m\n' "$1"; } | ||
| info() { printf ' %s\n' "$1"; } | ||
| die() { printf 'deploy: %s\n' "$1" >&2; exit 1; } | ||
|
|
||
| [ "${CLOUDFLARE_ACCOUNT_ID:-}" = "$EXPECTED_ACCOUNT_ID" ] || die \ | ||
| "CLOUDFLARE_ACCOUNT_ID must be pinned to erxes Inc ($EXPECTED_ACCOUNT_ID)" | ||
|
|
||
| step "Checking wrangler login" | ||
| if ! bunx wrangler whoami >/dev/null 2>&1; then | ||
| info "Not logged in. Run: bunx wrangler login" | ||
| exit 1 | ||
| bunx wrangler whoami >/dev/null 2>&1 || die "not logged in; run bunx wrangler login" | ||
| info "account: erxes Inc ($EXPECTED_ACCOUNT_ID)" | ||
|
|
||
| step "Provisioning D1 database '$DATABASE_NAME'" | ||
| DB_ID="$(bunx wrangler d1 list --json 2>/dev/null \ | ||
| | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const r=JSON.parse(s.slice(s.indexOf("["))).find(d=>d.name===process.argv[1]);process.stdout.write(r?r.uuid:"")}catch{}})' "$DATABASE_NAME")" | ||
| if [ -z "$DB_ID" ]; then | ||
| CREATE_OUT="$(bunx wrangler d1 create "$DATABASE_NAME" 2>&1)" | ||
| DB_ID="$(printf '%s' "$CREATE_OUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)" | ||
| info "created: $DB_ID" | ||
| else | ||
| info "reusing: $DB_ID" | ||
| fi | ||
| info "Logged in." | ||
|
|
||
| step "Provisioning D1 database 'executor'" | ||
| # `d1 create` is non-idempotent (errors if it exists), so list first. | ||
| EXISTING_ID="$(bunx wrangler d1 list --json 2>/dev/null \ | ||
| | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const r=JSON.parse(s).find(d=>d.name==="executor");process.stdout.write(r?r.uuid:"")}catch{}})')" | ||
| if [ -n "$EXISTING_ID" ]; then | ||
| DB_ID="$EXISTING_ID" | ||
| info "Reusing existing database: $DB_ID" | ||
| [ -n "$DB_ID" ] || die "failed to resolve D1 database id" | ||
|
|
||
| step "Provisioning R2 bucket '$BUCKET_NAME'" | ||
| if bunx wrangler r2 bucket list 2>/dev/null | grep -q "$BUCKET_NAME"; then | ||
| info "reusing existing bucket" | ||
| else | ||
| CREATE_OUT="$(bunx wrangler d1 create executor 2>&1)" | ||
| DB_ID="$(printf '%s' "$CREATE_OUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)" | ||
| info "Created database: $DB_ID" | ||
| bunx wrangler r2 bucket create "$BUCKET_NAME" >/dev/null | ||
| info "created" | ||
| fi | ||
| [ -n "$DB_ID" ] || { echo "Failed to resolve D1 database id" >&2; exit 1; } | ||
|
|
||
| step "Writing D1 id into wrangler.jsonc" | ||
| # Replace whatever database_id is present (placeholder or a prior id). | ||
| node -e ' | ||
| const fs=require("fs"),p=process.argv[1],id=process.argv[2]; | ||
| let t=fs.readFileSync(p,"utf8"); | ||
| t=t.replace(/("database_id":\s*")[^"]*(")/, `$1${id}$2`); | ||
| fs.writeFileSync(p,t); | ||
| ' "$CONFIG" "$DB_ID" | ||
| info "wrangler.jsonc -> $DB_ID" | ||
|
|
||
| step "Ensuring EXECUTOR_SECRET_KEY secret" | ||
| if bunx wrangler secret list 2>/dev/null | grep -q EXECUTOR_SECRET_KEY; then | ||
| info "Secret already set — leaving it." | ||
|
|
||
| step "Generating instance config" | ||
| cp "$SOURCE_CONFIG" "$CONFIG" | ||
| node - "$CONFIG" "$DB_ID" "$DATABASE_NAME" "$BUCKET_NAME" "$WORKER_NAME" "$EXECUTOR_DOMAIN" <<'NODE' | ||
| const fs = require("node:fs"); | ||
| const [path, dbId, dbName, bucketName, workerName, domain] = process.argv.slice(2); | ||
| let text = fs.readFileSync(path, "utf8"); | ||
| text = text.replace(/("name":\s*")[^"]*(")/, `$1${workerName}$2`); | ||
| text = text.replace(/("database_name":\s*")[^"]*(")/, `$1${dbName}$2`); | ||
| text = text.replace(/("database_id":\s*")[^"]*(")/, `$1${dbId}$2`); | ||
| text = text.replace(/("bucket_name":\s*")[^"]*(")/, `$1${bucketName}$2`); | ||
| text = text.replace( | ||
| /(\s*"main":\s*"src\/worker\.ts",)/, | ||
| `$1\n "routes": [{ "pattern": "${domain}", "custom_domain": true }],`, | ||
| ); | ||
| text = text.replace( | ||
| /(\s*"vars":\s*{)/, | ||
| `$1\n // Direct UI stays fail-closed until an erxes Access app replaces these values.\n` + | ||
| ` // Signed /os/* calls from Cloudflare OS verify before Access and work immediately.\n` + | ||
| ` "ACCESS_TEAM_DOMAIN": "invalid.cloudflareaccess.com",\n` + | ||
| ` "ACCESS_AUD": "not-configured",\n` + | ||
| ` "ADMIN_EMAILS": "amaraaamka0404@gmail.com",`, | ||
| ); | ||
| fs.writeFileSync(path, text); | ||
| NODE | ||
| info "$CONFIG" | ||
|
|
||
| step "Ensuring deployment secrets" | ||
| if [ ! -f "$SECRETS_FILE" ]; then | ||
| node - "$SECRETS_FILE" <<'NODE' | ||
| const { randomBytes } = require("node:crypto"); | ||
| const { writeFileSync } = require("node:fs"); | ||
| const path = process.argv[2]; | ||
| writeFileSync(path, JSON.stringify({ | ||
| EXECUTOR_SECRET_KEY: randomBytes(32).toString("hex"), | ||
| CLOUDFLARE_OS_AUTH_SECRET: randomBytes(32).toString("hex"), | ||
| }, null, 2) + "\n", { mode: 0o600 }); | ||
| NODE | ||
| info "generated $SECRETS_FILE" | ||
| else | ||
| SECRET="$(node -e 'console.log(require("node:crypto").randomBytes(32).toString("hex"))')" | ||
| printf '%s' "$SECRET" | bunx wrangler secret put EXECUTOR_SECRET_KEY >/dev/null | ||
| info "Generated + uploaded a fresh 32-byte key." | ||
| info "reusing $SECRETS_FILE" | ||
| fi | ||
| chmod 600 "$SECRETS_FILE" | ||
|
|
||
| step "Fetching Erxes introspection snapshot" | ||
| node scripts/fetch-erxes-introspection.mjs | ||
|
|
||
| step "Building the web SPA" | ||
| bunx vite build | ||
| node scripts/assert-shell-asset.mjs | ||
| cp assets/erxes-introspection.json dist/erxes-introspection.json | ||
| info "bundled erxes-introspection.json into dist/" | ||
|
|
||
| step "Deploying Worker" | ||
| bunx wrangler deploy | ||
|
|
||
| cat <<'NEXT' | ||
|
|
||
| ==> One manual step left: turn on Cloudflare Access (the auth layer) | ||
| if [ "${1:-}" = "--dry-run" ]; then | ||
| info "dry-run: skipped Worker deploy and secret upload" | ||
| exit 0 | ||
| fi | ||
|
|
||
| The Worker is deployed but is not ready to serve requests until you configure | ||
| a Cloudflare Access application. API and MCP requests return 503 and name the | ||
| missing variables until configuration is complete. In the Zero Trust | ||
| dashboard: | ||
| step "Deploying '$WORKER_NAME'" | ||
| bunx wrangler deploy -c "$CONFIG" | ||
|
|
||
| 1. Access -> Applications -> Add an application -> Self-hosted | ||
| 2. Application domain: executor-cloudflare.<your-subdomain>.workers.dev | ||
| 3. Add an Access policy (e.g. "Emails ending in @yourcompany.com") | ||
| 4. After saving, copy the Application Audience (AUD) tag, then set: | ||
| bunx wrangler deploy --var ACCESS_AUD:<aud> \ | ||
| --var ACCESS_TEAM_DOMAIN:<your-team>.cloudflareaccess.com \ | ||
| --var ADMIN_EMAILS:<admin@example.com> | ||
| step "Uploading secrets" | ||
| bunx wrangler secret bulk -c "$CONFIG" < "$SECRETS_FILE" >/dev/null | ||
| info "EXECUTOR_SECRET_KEY and CLOUDFLARE_OS_AUTH_SECRET uploaded" | ||
|
|
||
| Wrangler preserves these live variables during later code deploys. | ||
| cat <<NEXT | ||
|
|
||
| That's it. Visiting the Worker URL now prompts a Cloudflare Access login, | ||
| and the Worker validates the issued JWT on every request. | ||
| Executor is live at https://$EXECUTOR_DOMAIN | ||
|
|
||
| Direct browser access remains closed until an erxes Cloudflare Access app is configured. | ||
| Cloudflare OS calls to /os/* are ready now via the generated shared secret. | ||
| NEXT |
102 changes: 102 additions & 0 deletions
102
apps/host-cloudflare/scripts/fetch-erxes-introspection.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| #!/usr/bin/env node | ||
| // Fetch the OfficeNext GraphQL introspection snapshot for offline tool production. | ||
| // Writes apps/host-cloudflare/assets/erxes-introspection.json (wrapper: { data: ... }). | ||
| import { mkdirSync, writeFileSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const ENDPOINT = process.env.ERXES_GRAPHQL_URL ?? "https://officenext.erxes.io/gateway/graphql"; | ||
| const OUT = join(dirname(fileURLToPath(import.meta.url)), "../assets/erxes-introspection.json"); | ||
|
|
||
| const INTROSPECTION_QUERY = ` | ||
| query IntrospectionQuery { | ||
| __schema { | ||
| queryType { name } | ||
| mutationType { name } | ||
| types { | ||
| kind | ||
| name | ||
| description | ||
| fields(includeDeprecated: false) { | ||
| name | ||
| description | ||
| args { | ||
| name | ||
| description | ||
| type { ...TypeRef } | ||
| defaultValue | ||
| } | ||
| type { ...TypeRef } | ||
| } | ||
| inputFields { | ||
| name | ||
| description | ||
| type { ...TypeRef } | ||
| defaultValue | ||
| } | ||
| interfaces { ...TypeRef } | ||
| enumValues(includeDeprecated: false) { | ||
| name | ||
| description | ||
| } | ||
| possibleTypes { ...TypeRef } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fragment TypeRef on __Type { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| ofType { | ||
| kind | ||
| name | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const response = await fetch(ENDPOINT, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ query: INTROSPECTION_QUERY }), | ||
| }); | ||
| if (!response.ok) { | ||
| console.error(`Introspection failed: HTTP ${response.status}`); | ||
| process.exit(1); | ||
| } | ||
| const payload = await response.json(); | ||
| if (payload.errors?.length) { | ||
| console.error("Introspection errors:", payload.errors); | ||
| process.exit(1); | ||
| } | ||
| if (!payload.data?.__schema) { | ||
| console.error("Introspection response missing schema"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| mkdirSync(dirname(OUT), { recursive: true }); | ||
| writeFileSync(OUT, JSON.stringify(payload)); | ||
| console.log(`Wrote ${OUT} (${(JSON.stringify(payload).length / 1_048_576).toFixed(2)} MiB)`); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
issue (broader_impact): The package
deployscript still runs onlyvite buildand does not fetch or copyerxes-introspection.jsonintodist, so deployments throughbun run deployomit the offline snapshot and first connection provisioning falls back to live GraphQL introspection.Triggers: When the Worker is deployed through the package's
deployscript rather than the bespokescripts/deploy.shflow.Suggested fix: Make
deployinvoke the same snapshot-fetching build or explicitly runfetch-erxes-introspection.mjsand copy the asset beforewrangler deploy.