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
13 changes: 12 additions & 1 deletion .github/workflows/ai-sdk-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
- name: Install n8n dependencies
run: |
cd typescript && npm run build
cd ../n8n-nodes-metadata && npm install
cd ../n8n-nodes-metadata && npm install --legacy-peer-deps

# ==================== Linting (via Makefile) ====================

Expand Down Expand Up @@ -130,6 +130,11 @@ jobs:
npm run build
npm test 2>/dev/null || echo "No unit tests configured"

- name: "Scan: n8n Node"
id: scan-n8n
continue-on-error: true
run: npx @n8n/scan-community-package n8n-nodes-metadata

# ==================== Summary ====================

- name: Check Results
Expand All @@ -153,6 +158,11 @@ jobs:
echo "| TypeScript | ${{ steps.test-typescript.outcome == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Java | ${{ steps.test-java.outcome == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| n8n | ${{ steps.test-n8n.outcome == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### n8n Package Scan" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Community package scan | ${{ steps.scan-n8n.outcome == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY

# Fail if any step failed
FAILED=0
Expand All @@ -166,6 +176,7 @@ jobs:
if [ "${{ steps.test-typescript.outcome }}" != "success" ]; then echo "❌ TypeScript tests failed"; FAILED=1; fi
if [ "${{ steps.test-java.outcome }}" != "success" ]; then echo "❌ Java tests failed"; FAILED=1; fi
if [ "${{ steps.test-n8n.outcome }}" != "success" ]; then echo "❌ n8n tests failed"; FAILED=1; fi
if [ "${{ steps.scan-n8n.outcome }}" != "success" ]; then echo "⚠️ n8n package scan failed (requires package to be published on npm)"; fi

if [ $FAILED -eq 1 ]; then
echo ""
Expand Down
107 changes: 107 additions & 0 deletions .github/workflows/ai-sdk-release-n8n.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Release n8n Node

on:
push:
tags:
- 'n8n-v*'
workflow_dispatch:
inputs:
version:
required: true
type: string
description: 'Version to release (e.g., 0.2.0)'
workflow_call:
inputs:
version:
required: true
type: string
description: 'Version to release (e.g., 0.2.0)'

permissions:
contents: read
id-token: write

concurrency:
group: release-n8n-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-publish:
name: Build and Publish to npm
runs-on: ubuntu-latest
environment: publish
defaults:
run:
working-directory: n8n-nodes-metadata

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: n8n-nodes-metadata/package-lock.json

- name: Upgrade npm for trusted publishing
working-directory: .
run: |
npm install -g npm@latest
echo "npm version: $(npm --version)"

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Run linter
run: npm run lint

- name: Build
run: npm run build

- name: Run tests
run: npm test 2>/dev/null || echo "No unit tests configured"

- name: Extract version
id: version
run: |
INPUT_VERSION="${{ inputs.version || '' }}"
if [ -n "$INPUT_VERSION" ]; then
echo "version=$INPUT_VERSION" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF#refs/tags/n8n-v}" >> $GITHUB_OUTPUT
fi

- name: Verify package version matches tag
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
TAG_VERSION=${{ steps.version.outputs.version }}
PKG_BASE=$(echo "$PACKAGE_VERSION" | cut -d. -f1-3)
TAG_BASE=$(echo "$TAG_VERSION" | cut -d. -f1-3)
if [ "$PKG_BASE" != "$TAG_BASE" ]; then
echo "Error: package.json version ($PACKAGE_VERSION) does not match tag version ($TAG_VERSION)"
exit 1
fi

- name: Check if already published
id: check
run: |
if npm view n8n-nodes-metadata@${{ steps.version.outputs.version }} version 2>/dev/null; then
echo "Version ${{ steps.version.outputs.version }} already published to npm. Skipping."
echo "published=true" >> $GITHUB_OUTPUT
else
echo "published=false" >> $GITHUB_OUTPUT
fi

- name: Publish to npm
if: steps.check.outputs.published == 'false'
run: npm publish --access public --provenance

- name: Verify with n8n community package scanner
if: steps.check.outputs.published == 'false'
working-directory: .
run: |
sleep 10 # Wait for npm registry to propagate
npx --yes @n8n/scan-community-package n8n-nodes-metadata
11 changes: 10 additions & 1 deletion .github/workflows/ai-sdk-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ name: Release
# - Python SDK → PyPI
# - TypeScript SDK → npm
# - Java SDK → Maven Central
# - n8n Node → npm (after TypeScript SDK)
# - CLI binaries → attached to the GitHub Release
#
# For individual SDK releases, push SDK-specific tags instead:
# python-v0.2.0, typescript-v0.2.0, java-v0.2.0, cli-v0.2.0
# python-v0.2.0, typescript-v0.2.0, java-v0.2.0, n8n-v0.2.0, cli-v0.2.0

on:
release:
Expand Down Expand Up @@ -93,6 +94,14 @@ jobs:
version: ${{ needs.validate.outputs.version }}
secrets: inherit

release-n8n:
name: n8n Node
needs: [validate, release-typescript]
uses: ./.github/workflows/ai-sdk-release-n8n.yml
with:
version: ${{ needs.validate.outputs.version }}
secrets: inherit

release-cli:
name: CLI Binaries
needs: validate
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ release: ## Create a GitHub Release (usage: make release [B=branch])
@echo " - Publish Python SDK to PyPI"
@echo " - Publish TypeScript SDK to npm"
@echo " - Publish Java SDK to Maven Central"
@echo " - Publish n8n node to npm (after TypeScript SDK)"
@echo " - Build CLI binaries and attach to release"

# Development helpers
Expand Down
5 changes: 3 additions & 2 deletions n8n-nodes-metadata/credentials/AISdkApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ export class AISdkApi implements ICredentialType {
name = 'aiSdkApi';
displayName = 'OpenMetadata API';
documentationUrl = 'https://docs.open-metadata.org';
icon = 'file:metadata.png' as const;
properties: INodeProperties[] = [
{
displayName: 'Server URL',
name: 'serverUrl',
type: 'string',
default: '',
placeholder: 'https://your-openmetadata-instance.com',
description: 'The URL of your OpenMetadata instance',
description: 'The URL of your OpenMetadata instance.',
required: true,
},
{
Expand All @@ -27,7 +28,7 @@ export class AISdkApi implements ICredentialType {
password: true,
},
default: '',
description: 'JWT token for authentication (bot token or personal access token)',
description: 'JWT token for authentication (bot token or personal access token).',
required: true,
},
];
Expand Down
Binary file added n8n-nodes-metadata/credentials/metadata.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions n8n-nodes-metadata/nodes/AISdkAgent/AISdkAgent.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"node": "n8n-nodes-metadata.aiSdkAgent",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Miscellaneous"],
"alias": ["openmetadata", "metadata", "agent", "ai", "dynamic agent", "data catalog"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.open-metadata.org"
}
]
}
}
100 changes: 55 additions & 45 deletions n8n-nodes-metadata/nodes/AISdkAgent/AISdkAgent.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
INodeType,
INodeTypeDescription,
NodeApiError,
NodeConnectionTypes,
NodeOperationError,
} from 'n8n-workflow';

Expand All @@ -24,13 +25,14 @@ export class AISdkAgent implements INodeType {
icon: 'file:metadata.png',
group: ['transform'],
version: 1,
usableAsTool: true,
subtitle: '={{$parameter["agentName"]}}',
description: 'Invoke an OpenMetadata DynamicAgent',
description: 'Invoke an OpenMetadata DynamicAgent.',
defaults: {
name: 'AI SDK Agent',
},
inputs: ['main'],
outputs: ['main'],
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'aiSdkApi',
Expand All @@ -44,7 +46,7 @@ export class AISdkAgent implements INodeType {
type: 'string',
default: '',
placeholder: 'my-agent',
description: 'The name of the DynamicAgent to invoke',
description: 'The name of the DynamicAgent to invoke.',
required: true,
},
{
Expand All @@ -56,7 +58,7 @@ export class AISdkAgent implements INodeType {
},
default: '',
placeholder: 'Enter your message or query for the agent',
description: 'The message to send to the agent',
description: 'The message to send to the agent.',
required: true,
},
{
Expand All @@ -65,7 +67,7 @@ export class AISdkAgent implements INodeType {
type: 'string',
default: '',
placeholder: 'Optional: continue a conversation',
description: 'Optional conversation ID for multi-turn conversations',
description: 'Optional conversation ID for multi-turn conversations.',
required: false,
},
],
Expand Down Expand Up @@ -124,51 +126,59 @@ export class AISdkAgent implements INodeType {
pairedItem: { item: i },
});
} catch (error) {
// Convert SDK errors to n8n NodeApiError with descriptive messages
if (!(error instanceof NodeApiError) && !(error instanceof NodeOperationError)) {
const agentName = this.getNodeParameter('agentName', i) as string;
let errorMessage: string;
let httpCode: string;

if (error instanceof AuthenticationError) {
errorMessage = 'Authentication failed: Invalid or expired JWT token';
httpCode = '401';
} else if (error instanceof AgentNotEnabledError) {
errorMessage = 'Agent is not API-enabled. Enable API access in the OpenMetadata UI.';
httpCode = '403';
} else if (error instanceof AgentNotFoundError) {
errorMessage = `Agent "${agentName}" not found`;
httpCode = '404';
} else if (error instanceof AgentExecutionError) {
errorMessage = 'Agent execution failed. Check the agent configuration in OpenMetadata.';
httpCode = '500';
} else if (error instanceof AISdkError) {
errorMessage = error.message || 'OpenMetadata API error occurred';
httpCode = String(error.statusCode || 500);
} else {
const unknownError = error as { message?: string; statusCode?: number };
errorMessage = unknownError.message || 'Unknown error occurred';
httpCode = String(unknownError.statusCode || 500);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorPayload = { message: errorMessage } as any;
const nodeError = new NodeApiError(
this.getNode(),
errorPayload,
{ httpCode, itemIndex: i },
);

if (this.continueOnFail()) {
returnData.push({
json: { error: nodeError.message },
pairedItem: { item: i },
});
continue;
}
throw nodeError;
}

if (this.continueOnFail()) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
returnData.push({
json: { error: errorMessage },
json: { error: (error as NodeApiError).message },
pairedItem: { item: i },
});
continue;
}

if (error instanceof NodeApiError || error instanceof NodeOperationError) {
throw error;
}

// Convert SDK errors to n8n NodeApiError with descriptive messages
const agentName = this.getNodeParameter('agentName', i) as string;
let errorMessage: string;
let httpCode: string;

if (error instanceof AuthenticationError) {
errorMessage = 'Authentication failed: Invalid or expired JWT token';
httpCode = '401';
} else if (error instanceof AgentNotEnabledError) {
errorMessage = 'Agent is not API-enabled. Enable API access in the OpenMetadata UI.';
httpCode = '403';
} else if (error instanceof AgentNotFoundError) {
errorMessage = `Agent "${agentName}" not found`;
httpCode = '404';
} else if (error instanceof AgentExecutionError) {
errorMessage = 'Agent execution failed. Check the agent configuration in OpenMetadata.';
httpCode = '500';
} else if (error instanceof AISdkError) {
errorMessage = error.message || 'OpenMetadata API error occurred';
httpCode = String(error.statusCode || 500);
} else {
const unknownError = error as { message?: string; statusCode?: number };
errorMessage = unknownError.message || 'Unknown error occurred';
httpCode = String(unknownError.statusCode || 500);
}

throw new NodeApiError(this.getNode(), {
message: errorMessage,
httpCode,
}, {
itemIndex: i,
});
throw error;
}
}

Expand Down
Loading
Loading