Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ jobs:
# can't exercise isolation here (it returns no data and the gate fails spuriously). Run it
# locally with `benchmark/probe-fs.js` + `smoke-test/verify-isolation.sh` (both kept in repo).

# Memory-limit detection gate (same SANDBOX_CODE_ONLY stack): the OOM input expression blows
# Memory-limit detection gate (same SANDBOX_CODE_ONLY stack): the OOM code step blows
# the ENGINE heap via the isolate->engine result copy, and AP_WORKER_CONCURRENCY=1 (job env)
# gives that single sandbox the full 1G worker container. Every run must end
# MEMORY_LIMIT_EXCEEDED — this pins the detection path for production code-only engine OOMs.
- name: Setup OOM flow
if: matrix.executionMode == 'SANDBOX_CODE_ONLY'
id: setup-oom
run: |
FLOW_ID=$(CODE_INPUT_SUM="$(cat benchmark/oom-expression.txt)" benchmark/setup.sh)
FLOW_ID=$(CODE_BODY_FILE=benchmark/oom-code.js benchmark/setup.sh)
echo "flow_id=$FLOW_ID" >> "$GITHUB_OUTPUT"
echo "Flow ID: $FLOW_ID"

Expand Down
11 changes: 6 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,12 @@ COPY . .
# Build frontend, engine, server API, and worker
RUN NODE_OPTIONS=--max-old-space-size=4096 npx turbo run build --filter=web --filter=@activepieces/engine --filter=api --filter=worker

# The web build emits hidden source maps (vite build.sourcemap='hidden') used to
# symbolicate production stack traces in Sentry/BetterStack error tracking. Upload
# them here (cloud CI, guarded by a token) BEFORE stripping, then always remove the
# .map files so source is never served from the shipped image (self-hosted too).
# TODO(cloud-ci): inject + upload maps with sentry-cli when SENTRY_AUTH_TOKEN is set.
# Source maps are off unless AP_BUILD_SOURCEMAP=true: generating them costs ~1GB of
# peak heap in the web build (3.5GB vs 2.5GB measured) for ~21MB of output that this
# layer then deletes, which is what OOM-killed the image build. A build that opts in
# must upload them BEFORE this line; the delete stays so source is never served from
# the shipped image.
# TODO(cloud-ci): set AP_BUILD_SOURCEMAP=true and upload with sentry-cli when SENTRY_AUTH_TOKEN is set.
RUN find dist/packages/web -name '*.map' -delete

# Generate migration manifest (ordered list of migration names) for image-tag-based rollback
Expand Down
33 changes: 18 additions & 15 deletions benchmark/docker-compose.minio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,35 @@
# bucket must already exist — minio-init creates it before app/worker start.
services:
minio:
image: quay.io/minio/minio:RELEASE.2024-09-22T00-33-43Z
image: cgr.dev/chainguard/minio@sha256:bd014394a80898e68c149f2311fdf8d5a2c2f3bb2c33b9327ae6d02b4b065ae1
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
interval: 3s
timeout: 5s
retries: 20
networks:
- bench

minio-init:
image: quay.io/minio/mc:RELEASE.2024-09-16T17-43-14Z
minio-ready:
image: cgr.dev/chainguard/minio-client@sha256:b8b144ab34694ecea25aa352c4be9de4c26ee2a02701521dce02ee5593c57338
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 minioadmin minioadmin &&
mc mb --ignore-existing local/activepieces &&
echo 'bucket activepieces ready'
"
condition: service_started
environment:
MC_HOST_local: http://minioadmin:minioadmin@minio:9000
command: ready local
networks:
- bench

minio-init:
image: cgr.dev/chainguard/minio-client@sha256:b8b144ab34694ecea25aa352c4be9de4c26ee2a02701521dce02ee5593c57338
depends_on:
minio-ready:
condition: service_completed_successfully
environment:
MC_HOST_local: http://minioadmin:minioadmin@minio:9000
command: mb --ignore-existing local/activepieces
networks:
- bench

Expand Down
8 changes: 8 additions & 0 deletions benchmark/oom-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const code = async () => {
const chunk = 'x'.repeat(52428800);
const hog = [];
for (let i = 0; i < 64; i = i + 1) {
hog.push(chunk);
}
return hog;
};
1 change: 0 additions & 1 deletion benchmark/oom-expression.txt

This file was deleted.

36 changes: 19 additions & 17 deletions benchmark/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,6 @@ for i in $(seq 1 $MAX_RETRIES); do
sleep $RETRY_INTERVAL
done

# Wait for webhook piece to be synced (pieces sync from cloud in batches)
echo "Waiting for webhook piece to be available..." >&2
for i in $(seq 1 600); do
HAS_WEBHOOK=$(curl -sf "$BASE_URL/pieces" 2>/dev/null | jq '[.[].name] | any(. == "@activepieces/piece-webhook")' 2>/dev/null || echo "false")
if [ "$HAS_WEBHOOK" = "true" ]; then
echo "Webhook piece is available (took ${i}s)" >&2
break
fi
if [ "$i" -eq 600 ]; then
echo "ERROR: Webhook piece not available after 600s" >&2
exit 1
fi
sleep 1
done

# Sign up
echo "Authenticating..." >&2
BENCH_EMAIL="${BENCH_EMAIL:-bench@activepieces.com}"
Expand Down Expand Up @@ -81,6 +66,25 @@ echo "Signed up. Project: $PROJECT_ID" >&2

AUTH="Authorization: Bearer $TOKEN"

WEBHOOK_VERSION="${WEBHOOK_VERSION:-~0.1.36}"
MATH_VERSION="${MATH_VERSION:-~0.0.24}"

wait_for_piece() {
local name="$1" version="$2"
echo "Waiting for $name@$version to be available..." >&2
for i in $(seq 1 600); do
if curl -sf -G "$BASE_URL/pieces/$name" --data-urlencode "version=$version" -H "$AUTH" > /dev/null 2>&1; then
echo "$name@$version is available (took ${i}s)" >&2
return 0
fi
sleep 1
done
echo "ERROR: $name@$version not available after 600s" >&2
exit 1
}
wait_for_piece "@activepieces/piece-webhook" "$WEBHOOK_VERSION"
wait_for_piece "@activepieces/piece-math-helper" "$MATH_VERSION"

# Create flow
echo "Creating flow..." >&2
FLOW_RESPONSE=$(curl -s --fail-with-body "$BASE_URL/flows" \
Expand Down Expand Up @@ -117,8 +121,6 @@ else
fi

# Flow: webhook (latest) -> math-helper addition (latest) -> small CODE step -> return response (latest).
WEBHOOK_VERSION="${WEBHOOK_VERSION:-~0.1.36}"
MATH_VERSION="${MATH_VERSION:-~0.0.24}"
IMPORT_PAYLOAD=$(jq -n \
--arg code "$CODE_STEP_SRC" \
--arg webhookV "$WEBHOOK_VERSION" \
Expand Down
4 changes: 4 additions & 0 deletions brain/knowledge/engineering/ci-pr-review-hygiene.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,7 @@ Enforcement is the **`Codeowners review` repository ruleset** (active on the def
- **Mintlify-editor PRs arrive with `untitled-page*.mdx` at the docs root and `(1)`/`(2)`-suffixed media, wired into `docs.json` under those names.** The bot commits whatever the web editor autosaved, so pages land as `docs/untitled-page-5.mdx` rather than in the folder their nav group implies, and re-uploaded assets keep the browser's download-collision suffix (`data-flow-(1).mp4`, `01---insert-panel-(1.2x).mp4`). Mintlify's own link check passes — the names are internally consistent — so nothing flags it. Rename the files, fix the `docs.json` entries, then diff the *flattened nav tree* against `main` rather than reading the raw JSON diff: the editor also reindents, which buried a real change (the Enterprise Control `Security` group demoted to a child of `Guides`) inside 242 lines of whitespace churn on that PR.
- **After rewriting a lower branch of a stack, the `--onto` upstream you need is that branch's *old* tip, and `<branch>@{1}` is where to read it.** Every commit above a rewritten branch has to be replayed with `git rebase --onto <new lower tip> <old lower tip> <upper>`, and the old tip stops being reachable by name the moment the rebase finishes. Guessing a SHA from an earlier terminal scroll either fails outright with `invalid upstream` or, worse, succeeds against the wrong base and replays a handful of commits instead of all of them, which surfaces as a pile of conflicts the branch has with itself. `git rev-parse <branch>@{1}` reads it from the reflog every time. Re-run the whole `--onto` chain top-down after any lower-branch amend, and confirm afterwards that `git log --format=%s main..<top>` has no duplicate subjects.
- **Never commit a shared file onto a lower branch of a stack by copying it in from the stack top — you drag the upper branches' edits down with it.** Adding one gotcha bullet to a brain page while sitting on the top branch, then `git checkout <stash-or-top> -- <file>` after switching to the base branch, brings the *whole* file across, so every bullet the upper branches added to that page is silently duplicated onto the base. The commit stat is the tell: a one-line edit that reports six insertions is not a one-line edit. Switch branches first, re-apply the edit against **that branch's own copy** of the file, and re-check the stat before committing.
- **Cherry-picking a merged PR onto a release branch: the commit-msg hook rejects it, and `breaking-changes.mdx` conflicts pull in other people's entries.** GitHub writes the squash header server-side, so `fix(flows): … instead of reverting them (#14899)` never faced commitlint — replaying it locally trips the 100-character header limit and `git cherry-pick --continue` dies in husky with the working tree half-applied. Finish it with `git commit --no-verify -C <original-sha>` rather than rewording, so the release branch keeps the same subject as `main`. The `Unreleased` section of `docs/install/reference/breaking-changes.mdx` conflicts on almost every pick because it accumulates entries from unrelated `main` work: take only the section your own commit added. **Take it by its own heading, not by "everything after the marker" — the incoming side of that hunk usually runs on into `main`'s copy of an already-published `## X.Y.Z` section, and pasting the tail duplicates ~200 lines of published notes that no test, lint or typecheck will ever notice.** The reliable move is to rebuild the file: `git checkout origin/release/<version> -- docs/install/reference/breaking-changes.mdx`, then insert your entry under its own `## <new version>` heading above the previous release's, and confirm with `git diff --stat` that only your lines are added and `grep -c '^## '` that no version heading appears twice. Then run `bun install` before pushing — the release branch's `bun.lock` is usually stale against its own `package.json`s, and the pick drags those stale piece versions along until install corrects them; `git diff --stat origin/release/<version> -- bun.lock` should come back as just the packages your pick actually bumped. Expect the **PR size** gate to block it too: the picked commits drag their own upstream tests along (769/600 in `server/api` for two picks), and splitting a replay of already-reviewed work is the wrong answer — add `large-pr-ok`.
- **`api#test-ce` can exit 1 with every test passing — read the `Errors` line, not just `Tests`.** Vitest fails the run on *unhandled* errors too, so the summary reads `Test Files 92 passed`, `Tests 1076 passed`, `Errors 3 errors` and turbo reports `Failed: api#test-ce`. The recurring one is `TypeError: socket.destroySoon is not a function` at `Timeout.forceClose` in `@hono/node-server`, attributed to `test/integration/ce/mcp/mcp-platform-project-selection.test.ts` — a teardown race after the test already finished, not a failure of whatever you changed. Before chasing it, scroll to the `Unhandled Errors` block and check whether any test actually failed; if none did and the stack is in `forceClose`, re-run the job (`gh run rerun <id> --failed`) rather than editing code. Note the log section only has the whole `main` job, so `gh run view --job <id> --log-failed` buries it under thousands of lines of passing-test stderr — grep for `Unhandled Errors` or `Test Files`.
- **MinIO pulled its images from Docker Hub and then quay.io; the smoke test's S3 leg uses Chainguard's now.** Both moves failed the same way, `pull access denied` on Docker Hub and then `unauthorized: access to the requested resource is not authorized` on `quay.io/minio/*`. That reads like a rate limit or a missing secret, but it's neither, so no `docker login` or retry helps. `benchmark/docker-compose.minio.yml` pins `cgr.dev/chainguard/minio` and `cgr.dev/chainguard/minio-client` by digest. They are distroless, with no shell and no `mc` in the server image, so readiness is a one-shot `mc ready local` job and the bucket is a plain `mc mb`, both through `MC_HOST_local`. Don't add a healthcheck or an `sh -c` entrypoint there. This blocks **every** self-hosted release, because the S3 leg runs inside the release's own preflight. Check a vanished image with `docker manifest inspect <image>` before assuming credentials.
- **The web build spent ~1GB of peak heap on source maps the next Docker layer deleted.** `vite.config.mts` had `sourcemap: 'hidden'` for a Sentry upload that was never implemented — the Dockerfile ran `find dist/packages/web -name '*.map' -delete` right after, under its own unfinished `TODO(cloud-ci)`. Measured: **3.54GB peak RSS / 18.1s with maps, 2.53GB / 12.0s without** (382 map files, 21.5MB, for a 5MB main chunk). On a 2-vCPU/8GB depot runner, where turbo runs `web`, `api`, `worker` and `engine` builds concurrently in one container, that GB was the margin — `web:build` died with `Ineffective mark-compacts near heap limit` and took the release's preflight with it. It is **marginal, not deterministic**: the same commit builds fine on a retry, so a green build does not mean the headroom is there. Now behind `AP_BUILD_SOURCEMAP=true` (default off). Whoever implements the Sentry upload must set that flag *and* upload before the delete line.
1 change: 1 addition & 0 deletions brain/knowledge/engineering/web-feature-anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ way back was to stash the handful of intended edits, `git checkout` the package,
- **A panel that hand-rolls its draft state gets none of the form validation the rest of the app assumes.** react-hook-form + `zodResolver` is what surfaces `formErrors.required` and friends; a `useState` draft with a Save button has no schema, so the usual mistake is to *substitute* a fallback for an empty field (`name.trim().length > 0 ? name.trim() : existing.name`) instead of rejecting it. That reads as a silent failure: the request succeeds, the old value returns, and nothing explains why. When a surface cannot use react-hook-form, derive the invalid state, render the message next to the field, and disable the submit — do not paper over the empty value. Bit the AI Center key-detail panel while its sibling connect dialog, on a zod resolver, was correct. The second failure mode is that such a draft never resyncs: seeded once from a prop, it outlives any refetch of the row it mirrors, so a mutation that changes the row without changing its `key` (the AI Center replaces a key's credentials, and the panel is keyed on the config id) leaves the draft describing the old row — phantom "unsaved changes", and a save that reverts what the mutation just wrote. Bump a version segment into the `key` at the site that performs the mutation rather than diffing props inside the panel: TanStack Query hands back a new object identity on every refetch, so a naive identity comparison discards the admin's unsaved edits on a window refocus.
- **Sonner centres its icon against the whole toast, so a two-line toast puts the icon beside the wrong line.** `[data-sonner-toast]` is a centred flex row: fine for one line, visibly wrong the moment a description wraps or carries a disclosure — the icon drifts down next to the body instead of the title. Pass `classNames: { toast: 'items-start!', icon: 'mt-0.5' }` on that toast (the icon is 16px against 13px title text, so it needs the nudge to sit on the title's baseline). The `!` is not optional: sonner ships its own stylesheet, and a plain Tailwind `items-start` loses to it. Per-toast rather than on the `Toaster`, unless every toast in the app is meant to move.
- **To see a fetch-failure placeholder in the dev app, force the branch in code — do not try to break the network.** Patching `XMLHttpRequest.prototype.open` to rewrite the path (the api client is axios, so patching `fetch` alone does nothing) works only sometimes and costs a lot of fiddling: React Query keeps rendering the last good data, so the placeholder needs a query key with no cache; a full reload wipes the patch before the app boots, so navigation has to stay client-side; and some surfaces never error at all even when the rewritten path is confirmed to 404. Temporarily flipping the branch itself — `) : isError ? (` to `) : true || isError ? (` in `DataTable`, plus the same in each hand-written list — makes every page reachable by plain URL with no timing at all. Two cautions: it proves the *rendering* and not that `isError` is ever set, and `true || x` breaks TypeScript's narrowing after the guard, so a forced early return can throw "possibly undefined" errors into the Vite overlay — force it from the caller's prop instead when that happens. Forcing the branch is often not enough on a Community instance: agents, the AI Capabilities section and the embed subdomain steps are behind route guards, edition checks and `LockedFeatureGuard`, so those have to be forced open too (`AgentsFlagGuard`'s redirect, the `edition === ApEdition.COMMUNITY` branch in `routes/platform/setup/ai/index.tsx`, `isCloud` + `locked`) before the page renders at all. Revert with a grep for `true ||` / `false &&` / `locked={false}` before finishing.
- **Run eslint on web files from inside `packages/web`, never from the repo root.** From the root the `@/` alias does not resolve, so every import reports `import/no-unresolved`, and `--fix` silently moves relative imports (`./lib/...`) above the `@/` group. Then the package's own lint fails on `import/order`. Re-run `npx eslint --fix` from `packages/web` to put them back.

- **A table that ORs a secondary query into `isLoading` can never reach its error state.** The runs table passes `isLoading={isLoading || isFetchingFlows}`, where `isFetchingFlows` belongs to the flow list behind the *filter dropdown*. While that second query is fetching or retrying, the skeleton branch wins over `isError`, so a failing runs endpoint shows spinning rows rather than the placeholder — and a failing flows endpoint traps the table there indefinitely. Gate the skeleton on the query that owns the rows, and let a secondary query resolve on its own.

Expand Down
Loading
Loading