Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
20 changes: 16 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run test:unit

test-integration:
Expand All @@ -32,7 +35,10 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run test:integration

test-e2e:
Expand All @@ -42,7 +48,10 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run test:e2e

build:
Expand All @@ -53,5 +62,8 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run build
7 changes: 5 additions & 2 deletions .github/workflows/npm-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ jobs:
- name: Install dependencies
run: bun install

- name: Generate node catalog
run: bun run crawl-nodes
- name: Generate node catalog and schemas
run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}

- name: Build package
run: bun run build
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/schema-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Schema Update

on:
schedule:
- cron: '0 3 * * 1' # Monday 3AM UTC
workflow_dispatch:

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install

- name: Generate schemas
run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}

- run: bun run build

- name: Download latest release
run: |
mkdir -p /tmp/latest
bunx npm pack @elizaos/plugin-n8n-workflow@latest --pack-destination /tmp
tar xzf /tmp/elizaos-plugin-n8n-workflow-*.tgz -C /tmp/latest
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

First-release scenario will fail silently.

If @elizaos/plugin-n8n-workflow hasn't been published to npm yet, bunx npm pack @elizaos/plugin-n8n-workflow@latest will fail, aborting the entire job. Consider adding continue-on-error: true to this step and treating a missing release as "everything changed":

♻️ Suggested fix
       - name: Download latest release
+        id: download
+        continue-on-error: true
         run: |
           mkdir -p /tmp/latest
           bunx npm pack `@elizaos/plugin-n8n-workflow`@latest --pack-destination /tmp
           tar xzf /tmp/elizaos-plugin-n8n-workflow-*.tgz -C /tmp/latest

Then in the compare step, treat a download failure as "changed":

       - name: Compare with latest release
         id: compare
         run: |
+          if [ "${{ steps.download.outcome }}" = "failure" ]; then
+            echo "No previous release found — treating as changed"
+            echo "changed=true" >> $GITHUB_OUTPUT
+            exit 0
+          fi
           CHANGED=false
🤖 Prompt for AI Agents
In @.github/workflows/schema-update.yml around lines 27 - 31, Add
continue-on-error: true to the "Download latest release" step that runs bunx npm
pack `@elizaos/plugin-n8n-workflow`@latest so the job doesn't abort if the package
isn't published; detect the failure by checking the presence of the extracted
/tmp/latest folder or by setting a step output (e.g., download_succeeded) based
on the pack/tar exit status, then update the subsequent compare step to treat
download_succeeded=false (or missing /tmp/latest) as "changed" so the workflow
proceeds as a first-release case.


- name: Compare with latest release
id: compare
run: |
CHANGED=false

for file in defaultNodes.json schemaIndex.json triggerSchemaIndex.json; do
NEW="dist/data/$file"
OLD="/tmp/latest/package/dist/data/$file"

if [ ! -f "$NEW" ]; then
continue
fi
Comment on lines +42 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing local artifact skipped silently — may mask build failures.

If dist/data/$file doesn't exist (e.g., the crawl or build step failed partially), the loop continues without marking anything as changed. This means a broken build could produce an empty diff and no version bump, silently swallowing the problem. Consider logging a warning or failing if an expected file is absent after a successful build.

🤖 Prompt for AI Agents
In @.github/workflows/schema-update.yml around lines 42 - 44, The loop that
skips missing artifacts currently does `if [ ! -f "$NEW" ]; then continue; fi`
which silently hides build failures; change this to emit a clear failure or
warning by checking the same `$NEW` variable inside the loop and either (a)
`echo` a descriptive warning with the file name and set a non-success flag
(e.g., `MISSING=true`) so the workflow can fail after the loop, or (b)
immediately `echo "ERROR: missing expected artifact $NEW"` and `exit 1`; update
the surrounding logic to use the `MISSING` flag if chosen so the job fails when
any expected `dist/data/$file` is absent.


if [ ! -f "$OLD" ]; then
echo "$file: NEW file (not in latest release)"
CHANGED=true
continue
fi

if ! diff <(jq -S . "$NEW") <(jq -S . "$OLD") > /dev/null 2>&1; then
echo "$file: CHANGED"
CHANGED=true
else
echo "$file: unchanged"
fi
done

echo "changed=$CHANGED" >> $GITHUB_OUTPUT

- name: Bump version
if: steps.compare.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
npm version patch --no-git-tag-version
VERSION=$(jq -r .version package.json)
git add package.json
git commit -m "chore: bump to v${VERSION} (schema update)"
git push
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security concern: This workflow automatically pushes version bumps to main without creating a PR for review.

Risk: Schema changes could introduce breaking changes that bypass code review.

Recommendation: Instead of auto-pushing, create a PR:

- name: Create PR for schema update
  if: steps.compare.outputs.changed == 'true'
  run: |
    git checkout -b schema-update-$(date +%Y%m%d)
    git add package.json dist/data/
    git commit -m "chore: update schemas (automated)"
    git push origin schema-update-$(date +%Y%m%d)
    gh pr create --title "chore: update schemas" --body "Automated schema update from scheduled job"

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Trigger publish
if: steps.compare.outputs.changed == 'true'
run: gh workflow run npm-deploy.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ coverage/
.temp/
tmp/
temp/
# Generated node catalog (run `bun run crawl-nodes` to regenerate)
# Generated data files (run `bun run crawl` to regenerate)
src/data/defaultNodes.json
src/data/schemaIndex.json
src/data/triggerSchemaIndex.json

# Script cache
.cache/

22 changes: 22 additions & 0 deletions __tests__/fixtures/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ export function createSlackNode(overrides?: Partial<N8nNode>): N8nNode {
};
}

export function createGmailTriggerNode(overrides?: Partial<N8nNode>): N8nNode {
return {
name: 'Gmail Trigger',
type: 'n8n-nodes-base.gmailTrigger',
typeVersion: 1,
position: [250, 300],
parameters: {},
...overrides,
};
}

export function createGithubTriggerNode(overrides?: Partial<N8nNode>): N8nNode {
return {
name: 'GitHub Trigger',
type: 'n8n-nodes-base.githubTrigger',
typeVersion: 1,
position: [250, 300],
parameters: {},
...overrides,
};
}

// ============================================================================
// WORKFLOWS
// ============================================================================
Expand Down
112 changes: 111 additions & 1 deletion __tests__/integration/actions/createWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,45 @@ describe('CREATE_N8N_WORKFLOW action', () => {
const lastText = calls[calls.length - 1][0].text;
expect(lastText).toContain('Modified Workflow'); // modified workflow name

// Should store updated draft in cache
// Should store updated draft in cache with originMessageId
expect(runtime.setCache).toHaveBeenCalled();
const setCacheCall = (runtime.setCache as any).mock.calls.find((c: unknown[]) =>
(c[0] as string).startsWith('workflow_draft:')
);
expect(setCacheCall).toBeDefined();
const storedDraft = setCacheCall[1] as WorkflowDraft;
expect(storedDraft.originMessageId).toBe(message.id);
});

test('second call with same message.id after modify skips without callback (anti-loop)', async () => {
const draft = createDraftInCache();
draft.originMessageId = 'msg-001';

const mockService = createMockService();
const runtime = createMockRuntime({
services: { [N8N_WORKFLOW_SERVICE_TYPE]: mockService },
cache: { 'workflow_draft:user-001': draft },
});

const message = createMockMessage({ content: { text: 'Oui' } });
const callback = createMockCallback();

const result = await createWorkflowAction.handler(
runtime,
message,
createMockState(),
{},
callback
);

expect(result?.success).toBe(true);
expect(result?.data).toEqual({ awaitingUserInput: true });
// No service calls — skipped entirely
expect(mockService.modifyWorkflowDraft).not.toHaveBeenCalled();
expect(mockService.generateWorkflowDraft).not.toHaveBeenCalled();
expect(mockService.deployWorkflow).not.toHaveBeenCalled();
// No callback — agent gets no output to loop on
expect((callback as any).mock.calls.length).toBe(0);
});

test('expired draft is cleared and treated as new', async () => {
Expand Down Expand Up @@ -513,6 +550,79 @@ describe('CREATE_N8N_WORKFLOW action', () => {
});
});

// ==========================================================================
// MODIFY INCLUDES CHANGES IN PREVIEW
// ==========================================================================

describe('handler - modify includes changes in preview', () => {
test('preview data includes changed parameters after modify', async () => {
const draft: WorkflowDraft = {
workflow: {
name: 'Gmail Forward',
nodes: [
{
name: 'Gmail Trigger',
type: 'n8n-nodes-base.gmailTrigger',
typeVersion: 1,
position: [0, 0] as [number, number],
parameters: { pollTimes: { item: [{ mode: 'everyMinute' }] } },
},
{
name: 'Forward Email',
type: 'n8n-nodes-base.gmail',
typeVersion: 2,
position: [200, 0] as [number, number],
parameters: { operation: 'send', sendTo: '[email protected]' },
credentials: { gmailOAuth2Api: { id: 'cred-1', name: 'Gmail' } },
},
],
connections: {
'Gmail Trigger': { main: [[{ node: 'Forward Email', type: 'main', index: 0 }]] },
},
},
prompt: 'Forward emails',
userId: 'user-001',
createdAt: Date.now(),
};

const modifiedWorkflow = {
...draft.workflow,
nodes: [
draft.workflow.nodes[0],
{
...draft.workflow.nodes[1],
parameters: { operation: 'send', sendTo: '[email protected]' },
},
],
};

const mockService = createMockService({
modifyWorkflowDraft: mock(() => Promise.resolve(modifiedWorkflow)),
});

const runtime = createMockRuntime({
services: { [N8N_WORKFLOW_SERVICE_TYPE]: mockService },
useModel: createUseModelMock({ intent: 'modify', reason: 'User wants to modify' }),
cache: { 'workflow_draft:user-001': draft },
});

const callback = createMockCallback();

await createWorkflowAction.handler(
runtime,
createMockMessage({ content: { text: 'change email to [email protected]' } }),
createMockState(),
{ intent: 'modify', modification: 'change email to [email protected]' },
callback
);

// The callback text should contain the new email (changes are passed to formatActionResponse)
const calls = (callback as any).mock.calls;
const lastText = calls[calls.length - 1][0].text;
expect(lastText).toContain('[email protected]');
});
});

// ==========================================================================
// CALLBACK SUCCESS STATUS TESTS
// ==========================================================================
Expand Down
Loading
Loading