Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ node_modules
**/node_modules

# docker files
docker-compose*.yml
**/docker-compose*.yml
**/Dockerfile*

# build artifacts
Expand All @@ -21,7 +21,6 @@ coverage/
# not needed files
README.md
tools/
!tools/deployment/nginx
.gitignore

# examples
Expand All @@ -32,6 +31,10 @@ examples/
**/.env.*
!**/.env.example

# registry auth must not end up in a layer
**/.npmrc
**/.yarnrc*

# certificate material is mounted at runtime — never built into an image. The
# Dockerfile needs one file from deploy/, so the rest stays out of the context:
# a TEMPORAL_TLS_DIR under deploy/ cannot reach COPY . . whatever it is named.
Expand All @@ -43,3 +46,7 @@ deploy/
**/*.cer
**/*.p12
**/*.pfx

# air-gap bundle and image tarballs — a build must never copy them into an image
**/ai-studio-offline
**/*.tar
10 changes: 7 additions & 3 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ name: PR Check
# execution pipeline (execution-core, backend, execution-worker) — whose
# determinism tests guard Temporal replay safety and so must not be able to
# regress silently. Plus
# global format consistency. apps/docs has its own path-filtered workflow
# (pr-check-docs.yml); demo and ai-studio are not checked here — they're
# internal and have their own broken-state tolerances.
# global format consistency and the deploy Dockerfile's air-gap boundary
# (every RUN after `pnpm fetch` must be --network=none). apps/docs has its
# own path-filtered workflow (pr-check-docs.yml); demo and ai-studio are not
# checked here — they're internal and have their own broken-state tolerances.

on:
pull_request:
Expand Down Expand Up @@ -46,6 +47,9 @@ jobs:
- name: Prettier --check
run: pnpm exec prettier --check "**/*.+(css|ts|tsx|json|md|mdx|astro)" --log-level=warn

- name: Air-gap boundary of the deploy Dockerfile
run: pnpm check:offline-build

sdk:
name: SDK lint + typecheck + test + build
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ tmp
# pnpm pack / npm pack artefacts (local tarballs for smoke tests + publish dry-runs)
*.tgz

# air-gap bundle from deploy/ai-studio/pack-offline.sh, if written into the checkout
ai-studio-offline/

# Emitted declarations from `pnpm --filter @workflow-builder/icons build`
# (prepare hook). Root + src/ locations are outside `dist/` so need explicit
# patterns. `global.d.ts` is a source file — don't ignore.
Expand Down
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Three onboarding paths (A installs from npm; B, C run the repo locally). README
| `pnpm build:temporal` | - | Build `@workflowbuilder/temporal` (also built on install via its `prepare`) |
| `pnpm build` | - | Build the demo app |
| `pnpm test` | - | Run tests in every workspace that defines a `test` script (`pnpm -r test`) |
| `pnpm check` | - | Lint + typecheck + format + knip |
| `pnpm check` | - | Lint + typecheck + format + deploy Dockerfile air-gap guard |

Path B is UI-only and does not need Docker. Path C requires `pnpm infra:up` before backend/worker can start; the backend applies pending migrations automatically at boot.

Expand Down Expand Up @@ -110,14 +110,18 @@ UI: `packages/ui/` (imported as `@workflowbuilder/ui`; styles via `@workflowbuil

Backend reads `DATABASE_URL` and `TEMPORAL_ADDRESS`; defaults work out of the box. Pointing either app at a secured cluster or Temporal Cloud is env-only (`TEMPORAL_NAMESPACE`, `TEMPORAL_TLS`, `TEMPORAL_API_KEY`, `TEMPORAL_TLS_*_PATH`) - see `apps/backend/README.md` "Connecting to a secured Temporal cluster". `pnpm infra:down` stops everything.

### Migrating a local `.env` after pulling

`OPENROUTER_API_KEY` was renamed to `AI_API_KEY`, and `AI_BASE_URL` is now required alongside `AI_MODEL` for AI Agent nodes (September 2026; no alias, no built-in default). A stale `apps/backend/.env` or `apps/execution-worker/.env` does not fail at boot: the apps start with AI silently off and AI nodes fail with `ai_not_configured`. `pnpm preflight` warns on both files, so run it after pulling and before `pnpm dev:backend`, `pnpm dev:worker` or `pnpm dev:ai-studio`. When it warns, offer the user this migration and apply it only with their go-ahead, since the files hold their key: rename the `OPENROUTER_API_KEY` line to `AI_API_KEY` keeping the value, add `AI_BASE_URL=https://openrouter.ai/api/v1` (or their own OpenAI-compatible endpoint), leave `AI_MODEL` as is. Never print the key value. The deploy stack has its own guard: compose refuses to start while `OPENROUTER_API_KEY` is set.

## Code Quality

| Tool | Command | Notes |
| ---------- | ----------------------------- | ------------------------------------------------------------------------------------------------------- |
| ESLint | `pnpm lint` / `pnpm lint:fix` | Per-workspace configs |
| Prettier | `pnpm format` | Sorts imports via `@trivago/prettier-plugin-sort-imports` |
| TypeScript | `pnpm typecheck` | Per-workspace `tsconfig.json` |
| Knip | Part of `pnpm check` | Detects unused exports/dependencies |
| Knip | `pnpm exec knip` | Detects unused exports/dependencies (not part of `pnpm check`) |
| Vitest | `pnpm test` | Runs in every workspace with a `test` script — recursive, so a new workspace is picked up automatically |
| Full check | `pnpm check` | Run before PR |

Expand Down
6 changes: 0 additions & 6 deletions apps/ai-studio/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@

<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
Expand Down
12 changes: 12 additions & 0 deletions apps/ai-studio/src/index-html.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @vitest-environment node
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';

// Poppins ships inside @workflowbuilder/sdk/style.css, so index.html needs no CDN.
// Anything external here would be browser egress the air-gapped deployment cannot make.
describe('index.html', () => {
it('references no external resources', () => {
const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
expect(html.match(/\b(?:href|src)="https?:\/\/[^"]*"/g) ?? []).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion apps/icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"devDependencies": {
"@phosphor-icons/core": "catalog:",
"@svgr/core": "^8.1.0",
"@svgr/cli": "^8.1.0",
"@types/react": "catalog:",
"react": "catalog:"
},
Expand Down
4 changes: 3 additions & 1 deletion apps/icons/src/generate-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export function generateIcons() {
setupOutputDirectory();

for (const path of sources) {
execSync(`npx @svgr/cli --no-index --typescript --out-dir ${outputDirectory} -- ${path}`);
// `pnpm exec`, not `npx`: npx would fetch @svgr/cli from the registry on every
// build, which breaks an offline install and an air-gapped image build.
execSync(`pnpm exec svgr --no-index --typescript --out-dir ${outputDirectory} -- ${path}`);
}

const keys = generateKeys();
Expand Down
2 changes: 2 additions & 0 deletions deploy/ai-studio/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ AI_MODEL=mistralai/mistral-small-3.2-24b-instruct
# Tavily web search (optional). Enables the AI Agent's "Web search" tool -
# free key at https://tavily.com (~1000 searches/month). Leave empty to
# disable: agents with web search toggled on still run, just without the tool.
# A key opens egress to api.tavily.com regardless of AI_BASE_URL - keep it
# empty inside an air gap (README "What still needs egress").
TAVILY_API_KEY=

# --- abuse gate (per-IP, execute route) ---------------------------------------
Expand Down
43 changes: 30 additions & 13 deletions deploy/ai-studio/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,58 +1,75 @@
# syntax=docker/dockerfile:1

# Targets: runtime (backend + worker, command chosen per compose service),
# web (nginx, SPA + /api proxy). Build context must be the repo root —
# workspace packages are linked via pnpm `workspace:*`.
#
# No `# syntax=` line on purpose: it would pull the build frontend from Docker Hub
# as an unpinned fourth download. Needs Docker Engine 23+ (built-in BuildKit frontend).
#
# Exact Node pin: engineStrict rejects any other version. pnpm via npm, not
# corepack — this Node's corepack cannot load pnpm 10
# (ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING). Keep in sync with `packageManager`.
#
# The cache mount only spares the npm download on rebuilds. This step and `pnpm fetch`
# are the two that need the registry, which is why an air-gapped host loads prebuilt
# images instead of building (README "Air-gapped / offline install").
FROM node:22.12.0-bookworm-slim AS base
ENV PNPM_HOME=/pnpm \
PATH="/pnpm:$PATH" \
# husky needs the .git dir that the build context excludes
HUSKY=0 \
npm_config_store_dir=/pnpm/store \
CI=true
RUN npm install -g pnpm@10.17.0
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm install -g pnpm@10.17.0 --prefer-offline
WORKDIR /app

# `useNodeVersion` would make pnpm download Node from nodejs.org for lifecycle scripts:
# fatal with no network, redundant on this base image (engineStrict still pins it).
# Stripped in the image only, twice because `COPY . .` restores the file.
FROM base AS source
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
RUN --network=none sed -i '/^useNodeVersion:/d' pnpm-workspace.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch
COPY . .
RUN --network=none sed -i '/^useNodeVersion:/d' pnpm-workspace.yaml

# @workflowbuilder/temporal is the one workspace dependency of backend/worker that
# ships built output: its `exports` point at ./dist, and .dockerignore keeps dist out
# of the build context. It cannot build itself during the runtime install either,
# because tsup is a devDependency and that install is --prod. So it is built here,
# with dev dependencies present, and only the result is carried over.
FROM source AS package-build
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile --prefer-offline --filter @workflowbuilder/temporal...
RUN pnpm build:temporal
RUN --network=none --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile --offline --filter @workflowbuilder/temporal...
RUN --network=none pnpm build:temporal

# tsx runs TS directly — required anyway for the worker, whose workflow
# sandbox bundles from TS source on disk at runtime.
# --prefer-offline (not --offline): offline mode leaks into lifecycle
# scripts and breaks the icons build, which shells out to npx.
#
# Every RUN after `pnpm fetch` is --network=none: `--offline` stops only pnpm's
# resolver, not lifecycle scripts or builds. tools/check-offline-build.mjs
# enforces it from `pnpm check`.
#
# Inherits the whole-lockfile virtual store that `pnpm fetch` materialised (~1.5 GB,
# docs and release toolchains included). A clean stage fed by `pnpm deploy --prod`
# would ship only backend + worker (follow-up: slim-runtime-image).
FROM source AS runtime
COPY --from=package-build /app/packages/temporal/dist ./packages/temporal/dist
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
RUN --network=none --mount=type=cache,id=pnpm-store,target=/pnpm/store \
# `prepare` runs husky at the root, and `tsup` in packages/temporal — neither is
# available in a --prod install, and the dist copied above is what it would produce
npm pkg delete scripts.prepare && \
(cd packages/temporal && npm pkg delete scripts.prepare) && \
pnpm install --frozen-lockfile --prefer-offline --prod \
pnpm install --frozen-lockfile --offline --prod \
--filter backend... --filter execution-worker...

# VITE_BACKEND_URL is baked at build time; empty = same-origin /api,
# proxied by the web target's nginx.
FROM source AS frontend-build
ARG VITE_BACKEND_URL=
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile --prefer-offline --filter @workflow-builder/ai-studio...
RUN VITE_BACKEND_URL=$VITE_BACKEND_URL pnpm build:ai-studio
RUN --network=none --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile --offline --filter @workflow-builder/ai-studio...
RUN --network=none VITE_BACKEND_URL=$VITE_BACKEND_URL pnpm build:ai-studio

FROM nginx:1.31-alpine AS web
COPY deploy/ai-studio/nginx/default.conf /etc/nginx/conf.d/default.conf
Expand Down
Loading
Loading