watcher node

This commit is contained in:
JeongJaeHyun 2025-11-07 18:42:44 +09:00
commit 1ba8d53cd2
6 changed files with 8257 additions and 7599 deletions

331
README.md
View file

@ -1,247 +1,84 @@
![Banner image](https://user-images.githubusercontent.com/10284570/173569848-c624317f-42b1-45a6-ab09-f0ea3c247648.png) n8n-nodes-workflow-checker
This is an n8n community node that checks for updates in your workflow nodes.
# n8n-nodes-starter It monitors both:
This starter repository helps you build custom integrations for [n8n](https://n8n.io). It includes example nodes, credentials, the node linter, and all the tooling you need to get started. Community nodes from npm
Base nodes from n8n's official GitHub repository
## Quick Start
n8n is a fair-code licensed workflow automation platform.
> [!TIP] Installation
> **New to building n8n nodes?** The fastest way to get started is with `npm create @n8n/node`. This command scaffolds a complete node package for you using the [@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli). Follow the installation guide in the n8n community nodes documentation.
Community Node Installation
**To create a new node package from scratch:**
Go to Settings > Community Nodes
```bash Select Install
npm create @n8n/node Enter n8n-nodes-workflow-checker in the Enter npm package name field
``` Agree to the risks and select Install
**Already using this starter? Start developing with:** After installation, the Workflow Node Checker node will be available in your n8n workflow editor.
Prerequisites
```bash You need a Firecrawl API key to use this node.
npm run dev Credentials
``` This node requires Firecrawl API credentials:
This starts n8n with your nodes loaded and hot reload enabled. Get your API key from Firecrawl
In n8n, go to Credentials and create a new Firecrawl API credential
## What's Included Enter your API key
This starter repository includes two example nodes to learn from: Operations
The Workflow Node Checker node supports the following operations:
- **[Example Node](nodes/Example/)** - A simple starter node that shows the basic structure with a custom `execute` method Check All Nodes
- **[GitHub Issues Node](nodes/GithubIssues/)** - A complete, production-ready example built using the **declarative style**: Checks both community and base nodes in your workflow for updates.
- **Low-code approach** - Define operations declaratively without writing request logic Check Community Nodes Only
- Multiple resources (Issues, Comments) Checks only community nodes (from npm) for updates.
- Multiple operations (Get, Get All, Create) Check Base Nodes Only
- Two authentication methods (OAuth2 and Personal Access Token) Checks only n8n base nodes (from GitHub) for updates.
- List search functionality for dynamic dropdowns Options
- Proper error handling and typing
- Ideal for HTTP API-based integrations Check Delay (ms): Delay between checks to avoid rate limiting (default: 1500ms)
Include Content Preview: Include content preview in results (default: true)
> [!TIP] Days to Check: Number of days to check for recent updates (default: 30)
> The declarative/low-code style (used in GitHub Issues) is the recommended approach for building nodes that interact with HTTP APIs. It significantly reduces boilerplate code and handles requests automatically.
Outputs
Browse these examples to understand both approaches, then modify them or create your own. The node returns:
Summary (First Output)
## Finding Inspiration
Total nodes in workflow
Looking for more examples? Check out these resources: Number of community vs base nodes
Nodes checked successfully
- **[npm Community Nodes](https://www.npmjs.com/search?q=keywords:n8n-community-node-package)** - Browse thousands of community-built nodes on npm using the `n8n-community-node-package` tag Nodes with recent updates
- **[n8n Built-in Nodes](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes)** - Study the source code of n8n's official nodes for production-ready patterns and best practices Timestamp
- **[n8n Credentials](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/credentials)** - See how authentication is implemented for various services
Community Node Results
These are excellent resources to understand how to structure your nodes, handle different API patterns, and implement advanced features.
Package name
## Prerequisites Current version
Last published date
Before you begin, install the following on your development machine: npm URL
Content preview (if enabled)
### Required
Base Node Results
- **[Node.js](https://nodejs.org/)** (v22 or higher) and npm
- Linux/Mac/WSL: Install via [nvm](https://github.com/nvm-sh/nvm) Node name and type
- Windows: Follow [Microsoft's NodeJS guide](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows) Whether mentioned in recent releases
- **[git](https://git-scm.com/downloads)** Whether mentioned in recent commits
GitHub URLs
### Recommended Context snippets (if enabled)
- Follow n8n's [development environment setup guide](https://docs.n8n.io/integrations/creating-nodes/build/node-development-environment/) Usage Example
> [!NOTE] Add the Workflow Node Checker node to your workflow
> The `@n8n/node-cli` is included as a dev dependency and will be installed automatically when you run `npm install`. The CLI includes n8n for local development, so you don't need to install n8n globally. Connect your Firecrawl API credentials
Select an operation (e.g., "Check All Nodes")
## Getting Started with this Starter Execute the workflow
Review the results to see which nodes have updates
Follow these steps to create your own n8n community node package:
Compatibility
### 1. Create Your Repository Tested with n8n version 1.0.0 and above.
Resources
[Generate a new repository](https://github.com/n8n-io/n8n-nodes-starter/generate) from this template, then clone it:
n8n community nodes documentation
```bash Firecrawl Documentation
git clone https://github.com/<your-organization>/<your-repo-name>.git
cd <your-repo-name> License
``` MIT
### 2. Install Dependencies
```bash
npm install
```
This installs all required dependencies including the `@n8n/node-cli`.
### 3. Explore the Examples
Browse the example nodes in [nodes/](nodes/) and [credentials/](credentials/) to understand the structure:
- Start with [nodes/Example/](nodes/Example/) for a basic node
- Study [nodes/GithubIssues/](nodes/GithubIssues/) for a real-world implementation
### 4. Build Your Node
Edit the example nodes to fit your use case, or create new node files by copying the structure from [nodes/Example/](nodes/Example/).
> [!TIP]
> If you want to scaffold a completely new node package, use `npm create @n8n/node` to start fresh with the CLI's interactive generator.
### 5. Configure Your Package
Update `package.json` with your details:
- `name` - Your package name (must start with `n8n-nodes-`)
- `author` - Your name and email
- `repository` - Your repository URL
- `description` - What your node does
Make sure your node is registered in the `n8n.nodes` array.
### 6. Develop and Test Locally
Start n8n with your node loaded:
```bash
npm run dev
```
This command runs `n8n-node dev` which:
- Builds your node with watch mode
- Starts n8n with your node available
- Automatically rebuilds when you make changes
- Opens n8n in your browser (usually http://localhost:5678)
You can now test your node in n8n workflows!
> [!NOTE]
> Learn more about CLI commands in the [@n8n/node-cli documentation](https://www.npmjs.com/package/@n8n/node-cli).
### 7. Lint Your Code
Check for errors:
```bash
npm run lint
```
Auto-fix issues when possible:
```bash
npm run lint:fix
```
### 8. Build for Production
When ready to publish:
```bash
npm run build
```
This compiles your TypeScript code to the `dist/` folder.
### 9. Prepare for Publishing
Before publishing:
1. **Update documentation**: Replace this README with your node's documentation. Use [README_TEMPLATE.md](README_TEMPLATE.md) as a starting point.
2. **Update the LICENSE**: Add your details to the [LICENSE](LICENSE.md) file.
3. **Test thoroughly**: Ensure your node works in different scenarios.
### 10. Publish to npm
Publish your package to make it available to the n8n community:
```bash
npm publish
```
Learn more about [publishing to npm](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry).
### 11. Submit for Verification (Optional)
Get your node verified for n8n Cloud:
1. Ensure your node meets the [requirements](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/):
- Uses MIT license ✅ (included in this starter)
- No external package dependencies
- Follows n8n's design guidelines
- Passes quality and security review
2. Submit through the [n8n Creator Portal](https://creators.n8n.io/nodes)
**Benefits of verification:**
- Available directly in n8n Cloud
- Discoverable in the n8n nodes panel
- Verified badge for quality assurance
- Increased visibility in the n8n community
## Available Scripts
This starter includes several npm scripts to streamline development:
| Script | Description |
| --------------------- | ---------------------------------------------------------------- |
| `npm run dev` | Start n8n with your node and watch for changes (runs `n8n-node dev`) |
| `npm run build` | Compile TypeScript to JavaScript for production (runs `n8n-node build`) |
| `npm run build:watch` | Build in watch mode (auto-rebuild on changes) |
| `npm run lint` | Check your code for errors and style issues (runs `n8n-node lint`) |
| `npm run lint:fix` | Automatically fix linting issues when possible (runs `n8n-node lint --fix`) |
| `npm run release` | Create a new release (runs `n8n-node release`) |
> [!TIP]
> These scripts use the [@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli) under the hood. You can also run CLI commands directly, e.g., `npx n8n-node dev`.
## Troubleshooting
### My node doesn't appear in n8n
1. Make sure you ran `npm install` to install dependencies
2. Check that your node is listed in `package.json` under `n8n.nodes`
3. Restart the dev server with `npm run dev`
4. Check the console for any error messages
### Linting errors
Run `npm run lint:fix` to automatically fix most common issues. For remaining errors, check the [n8n node development guidelines](https://docs.n8n.io/integrations/creating-nodes/).
### TypeScript errors
Make sure you're using Node.js v22 or higher and have run `npm install` to get all type definitions.
## Resources
- **[n8n Node Documentation](https://docs.n8n.io/integrations/creating-nodes/)** - Complete guide to building nodes
- **[n8n Community Forum](https://community.n8n.io/)** - Get help and share your nodes
- **[@n8n/node-cli Documentation](https://www.npmjs.com/package/@n8n/node-cli)** - CLI tool reference
- **[n8n Creator Portal](https://creators.n8n.io/nodes)** - Submit your node for verification
- **[Submit Community Nodes Guide](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/)** - Verification requirements and process
## Contributing
Have suggestions for improving this starter? [Open an issue](https://github.com/n8n-io/n8n-nodes-starter/issues) or submit a pull request!
## License
[MIT](https://github.com/n8n-io/n8n-nodes-starter/blob/master/LICENSE.md)

View file

@ -1,78 +0,0 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
export class Example implements INodeType {
description: INodeTypeDescription = {
displayName: 'Example',
name: 'example',
icon: { light: 'file:example.svg', dark: 'file:example.dark.svg' },
group: ['input'],
version: 1,
description: 'Basic Example Node',
defaults: {
name: 'Example',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
properties: [
// Node properties which the user gets displayed and
// can change on the node.
{
displayName: 'My String',
name: 'myString',
type: 'string',
default: '',
placeholder: 'Placeholder value',
description: 'The description text',
},
],
};
// The function below is responsible for actually doing whatever this node
// is supposed to do. In this case, we're just appending the `myString` property
// with whatever the user has entered.
// You can make async calls and use `await`.
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
let item: INodeExecutionData;
let myString: string;
// Iterates over all input items and add the key "myString" with the
// value the parameter "myString" resolves to.
// (This could be a different value for each item in case it contains an expression)
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
myString = this.getNodeParameter('myString', itemIndex, '') as string;
item = items[itemIndex];
item.json.myString = myString;
} catch (error) {
// This node should never fail but we want to showcase how
// to handle errors.
if (this.continueOnFail()) {
items.push({ json: this.getInputData(itemIndex)[0].json, error, pairedItem: itemIndex });
} else {
// Adding `itemIndex` allows other workflows to handle this error
if (error.context) {
// If the error thrown already contains the context property,
// only append the itemIndex
error.context.itemIndex = itemIndex;
throw error;
}
throw new NodeOperationError(this.getNode(), error, {
itemIndex,
});
}
}
}
return [items];
}
}

View file

@ -0,0 +1,137 @@
import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { nodeUpdateCheckerMethods } from './methods/methods';
export class WorkflowNodeChecker implements INodeType {
description: INodeTypeDescription = {
displayName: 'Workflow Node Checker',
name: 'workflowNodeChecker',
icon: 'fa:search',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Check updates for community and base nodes in workflow',
defaults: {
name: 'Workflow Node Checker',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'firecrawlApi',
required: false,
displayOptions: {
show: {
'options.extractPatchNotes': [true],
},
},
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Check All Nodes',
value: 'checkAll',
description: 'Check updates for all nodes in the workflow',
action: 'Check all nodes for updates',
},
{
name: 'Check Community Nodes Only',
value: 'checkCommunity',
description: 'Check only community nodes from npm',
action: 'Check community nodes only',
},
{
name: 'Check Base Nodes Only',
value: 'checkBase',
description: 'Check only n8n base nodes from GitHub',
action: 'Check base nodes only',
},
],
default: 'checkAll',
},
{
displayName: 'Output Format',
name: 'outputFormat',
type: 'options',
options: [
{
name: 'JSON',
value: 'json',
description: 'Return as JSON objects',
},
{
name: 'Email Text',
value: 'emailText',
description: 'Return formatted text for email',
},
{
name: 'Both',
value: 'both',
description: 'Return both JSON and formatted text',
},
],
default: 'both',
},
{
displayName: 'Previous Versions (JSON)',
name: 'previousVersions',
type: 'json',
default: '{}',
description: 'Previous versions to compare against (format: {"packageName": "1.0.0"})',
placeholder: '{"@username/n8n-nodes-custom": "1.0.0"}',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Use Static Data Storage',
name: 'useStaticData',
type: 'boolean',
default: true,
description: 'Whether to automatically store and load versions from workflow static data (no external DB needed)',
},
{
displayName: 'Check Delay (ms)',
name: 'checkDelay',
type: 'number',
default: 1500,
description: 'Delay between checks to avoid rate limiting',
},
{
displayName: 'Fetch GitHub Repository',
name: 'fetchGithubRepo',
type: 'boolean',
default: true,
description: 'Whether to fetch GitHub repository links for community nodes from npm registry',
},
{
displayName: 'Extract Patch Notes',
name: 'extractPatchNotes',
type: 'boolean',
default: false,
description: 'Whether to extract patch notes for base nodes (requires Firecrawl API)',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
return await nodeUpdateCheckerMethods.execute.call(this);
}
}

661
nodes/methods/methods.ts Normal file
View file

@ -0,0 +1,661 @@
import { IExecuteFunctions, INodeExecutionData, NodeOperationError, IDataObject } from 'n8n-workflow';
// Helper function for delays
const sleep = (ms: number): Promise<void> => {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
};
// Helper function for GitHub API calls
async function fetchGitHubAPI(url: string): Promise<any> {
const response = await fetch(url, {
headers: {
'Accept': 'application/vnd.github+json',
'User-Agent': 'n8n-node-checker',
}
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
return await response.json();
}
interface CommunityNodeResult extends IDataObject {
type: 'community';
nodeType: string;
packageName: string;
previousVersion?: string;
currentVersion: string;
hasUpdate: boolean;
npmUrl: string;
githubUrl?: string;
lastPublished: string;
changelog?: string;
success: boolean;
error?: string;
}
interface BaseNodeResult extends IDataObject {
type: 'base';
nodeType: string;
nodeName: string;
mentionedInRecentReleases: boolean;
mentionedInRecentCommits: boolean;
patchNotes?: string;
githubReleasesUrl: string;
githubCommitsUrl: string;
success: boolean;
error?: string;
}
export const nodeUpdateCheckerMethods = {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const returnData: INodeExecutionData[] = [];
// Get parameters
const operation = this.getNodeParameter('operation', 0) as string;
const outputFormat = this.getNodeParameter('outputFormat', 0) as string;
const previousVersionsInput = this.getNodeParameter('previousVersions', 0, '{}') as string;
const useStaticData = this.getNodeParameter('options.useStaticData', 0, false) as boolean;
let previousVersions: Record<string, string> = {};
// Load from static data or parameter
if (useStaticData) {
const staticData = this.getWorkflowStaticData('global');
previousVersions = (staticData.nodeVersions as Record<string, string>) || {};
} else {
try {
previousVersions = JSON.parse(previousVersionsInput);
} catch (error) {
throw new NodeOperationError(
this.getNode(),
'Invalid JSON format for Previous Versions',
);
}
}
const options = this.getNodeParameter('options', 0, {}) as {
checkDelay?: number;
includePreview?: boolean;
fetchGithubRepo?: boolean;
extractPatchNotes?: boolean;
};
const checkDelay = options.checkDelay || 1500;
const fetchGithubRepo = options.fetchGithubRepo !== false;
const extractPatchNotes = options.extractPatchNotes !== false;
// Get current workflow
const workflow = this.getWorkflow();
// Get all nodes from the workflow
let allNodes: any[] = [];
try {
const workflowData = workflow as any;
if (workflowData.nodes && Array.isArray(workflowData.nodes)) {
allNodes = workflowData.nodes;
} else if (workflowData.nodes && typeof workflowData.nodes === 'object') {
allNodes = Object.values(workflowData.nodes);
} else {
allNodes = [];
}
} catch (error) {
allNodes = [];
}
// Separate community nodes and base nodes
const communityNodes: string[] = [];
const baseNodes: string[] = [];
for (const node of allNodes) {
if (!node || typeof node !== 'object') continue;
const nodeData = node as any;
const nodeType = nodeData.type;
if (!nodeType) continue;
if (nodeType.startsWith('@') || nodeType.includes('n8n-nodes-')) {
communityNodes.push(nodeType);
} else {
baseNodes.push(nodeType);
}
}
const communityResults: CommunityNodeResult[] = [];
const baseResults: BaseNodeResult[] = [];
try {
// Check community nodes on npm
if (operation === 'checkAll' || operation === 'checkCommunity') {
for (const nodeType of [...new Set(communityNodes)]) {
try {
const result = await checkCommunityNode(
nodeType,
previousVersions,
fetchGithubRepo,
checkDelay,
);
communityResults.push(result);
await sleep(checkDelay);
} catch (error) {
communityResults.push({
type: 'community',
nodeType,
packageName: nodeType.split('.')[0],
currentVersion: 'unknown',
hasUpdate: false,
npmUrl: '',
lastPublished: 'unknown',
error: error instanceof Error ? error.message : String(error),
success: false,
});
}
}
}
// Check base nodes on n8n GitHub
if ((operation === 'checkAll' || operation === 'checkBase') && baseNodes.length > 0) {
try {
const baseNodeResults = await checkBaseNodes(
baseNodes,
extractPatchNotes,
checkDelay,
);
baseResults.push(...baseNodeResults);
} catch (error) {
baseResults.push({
type: 'base',
nodeType: 'all',
nodeName: 'all',
mentionedInRecentReleases: false,
mentionedInRecentCommits: false,
githubReleasesUrl: '',
githubCommitsUrl: '',
error: error instanceof Error ? error.message : String(error),
success: false,
});
}
}
// Generate outputs based on format
if (outputFormat === 'json' || outputFormat === 'both') {
// Add JSON results
for (const result of communityResults) {
returnData.push({ json: result as IDataObject });
}
for (const result of baseResults) {
returnData.push({ json: result as IDataObject });
}
}
if (outputFormat === 'emailText' || outputFormat === 'both') {
// Generate email text
const emailText = generateEmailText(communityResults, baseResults, workflow.name || 'Workflow');
returnData.push({
json: {
type: 'email_report',
emailText,
workflowName: workflow.name || 'Workflow',
checkedAt: new Date().toISOString(),
},
});
}
// Add summary
const summary = {
totalNodes: allNodes.length,
communityNodesCount: communityNodes.length,
baseNodesCount: baseNodes.length,
communityNodesWithUpdates: communityResults.filter(r => r.hasUpdate).length,
baseNodesWithUpdates: baseResults.filter(
r => r.mentionedInRecentReleases || r.mentionedInRecentCommits,
).length,
checkedAt: new Date().toISOString(),
};
// Save current versions to static data if enabled
if (useStaticData) {
const staticData = this.getWorkflowStaticData('global');
const currentVersions: Record<string, string> = {};
for (const result of communityResults) {
if (result.success && result.currentVersion !== 'unknown') {
currentVersions[result.packageName] = result.currentVersion;
}
}
staticData.nodeVersions = currentVersions;
// Add info about saved versions
returnData.push({
json: {
type: 'version_storage',
message: 'Versions saved to workflow static data',
savedVersions: currentVersions,
savedAt: new Date().toISOString(),
},
});
}
returnData.unshift({
json: {
summary,
workflowName: workflow.name || 'Workflow',
operation,
outputFormat,
},
});
} catch (error: unknown) {
if (this.continueOnFail()) {
returnData.push({
json: {
success: false,
error: error instanceof Error ? error.message : String(error),
},
});
} else {
throw new NodeOperationError(this.getNode(), error as Error);
}
}
return [returnData];
},
};
// Helper functions
async function checkCommunityNode(
nodeType: string,
previousVersions: Record<string, string>,
fetchGithubRepo: boolean,
checkDelay: number,
): Promise<CommunityNodeResult> {
const packageName = nodeType.split('.')[0];
const npmUrl = `https://www.npmjs.com/package/${packageName}`;
// Use npm registry API
const registryUrl = `https://registry.npmjs.org/${packageName}`;
try {
const response = await fetch(registryUrl);
if (!response.ok) {
throw new Error(`npm registry returned ${response.status}`);
}
const registryData = await response.json() as any;
// Get latest version
const currentVersion = registryData['dist-tags']?.latest || 'unknown';
// Get last publish time
const timeData = registryData.time || {};
const publishedAt = timeData[currentVersion];
let lastPublished = 'unknown';
if (publishedAt) {
const publishDate = new Date(publishedAt);
const now = new Date();
const diffMs = now.getTime() - publishDate.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours === 0) {
const diffMinutes = Math.floor(diffMs / (1000 * 60));
lastPublished = `${diffMinutes} minutes ago`;
} else {
lastPublished = `${diffHours} hours ago`;
}
} else if (diffDays === 1) {
lastPublished = '1 day ago';
} else if (diffDays < 30) {
lastPublished = `${diffDays} days ago`;
} else if (diffDays < 365) {
const months = Math.floor(diffDays / 30);
lastPublished = months === 1 ? '1 month ago' : `${months} months ago`;
} else {
const years = Math.floor(diffDays / 365);
lastPublished = years === 1 ? '1 year ago' : `${years} years ago`;
}
}
// Check for version change
const previousVersion = previousVersions[packageName];
const hasUpdate = previousVersion ? previousVersion !== currentVersion : false;
// Get GitHub URL from repository field
let githubUrl: string | undefined;
if (fetchGithubRepo) {
const repository = registryData.repository;
if (repository) {
let repoUrl = typeof repository === 'string' ? repository : repository.url;
if (repoUrl) {
// Clean up git+https:// and .git
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '');
if (repoUrl.includes('github.com')) {
githubUrl = repoUrl;
}
}
}
}
// Try to get changelog from GitHub API if there's an update
let changelog: string | undefined;
if (hasUpdate && githubUrl) {
try {
await sleep(checkDelay);
changelog = await getChangelogFromGitHub(
githubUrl,
previousVersion || '',
currentVersion,
);
} catch (error) {
// Changelog fetch failed, continue without it
}
}
return {
type: 'community',
nodeType,
packageName,
previousVersion,
currentVersion,
hasUpdate,
npmUrl,
githubUrl,
lastPublished,
changelog,
success: true,
};
} catch (error) {
return {
type: 'community',
nodeType,
packageName,
previousVersion: previousVersions[packageName],
currentVersion: 'unknown',
hasUpdate: false,
npmUrl,
lastPublished: 'unknown',
error: error instanceof Error ? error.message : String(error),
success: false,
};
}
}
async function getChangelogFromGitHub(
githubUrl: string,
previousVersion: string,
currentVersion: string,
): Promise<string | undefined> {
try {
// Extract owner and repo from GitHub URL
const match = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (!match) return undefined;
const [, owner, repo] = match;
// Fetch releases from GitHub API
const releases = await fetchGitHubAPI(
`https://api.github.com/repos/${owner}/${repo}/releases`
);
if (!Array.isArray(releases) || releases.length === 0) {
return undefined;
}
// Find release matching current version
const currentRelease = releases.find((r: any) =>
r.tag_name === currentVersion ||
r.tag_name === `v${currentVersion}` ||
r.name?.includes(currentVersion)
);
if (!currentRelease || !currentRelease.body) {
return undefined;
}
// Return changelog, truncated if too long
let changelog = currentRelease.body;
if (changelog.length > 2000) {
changelog = changelog.substring(0, 2000) + '...';
}
return changelog;
} catch (error) {
return undefined;
}
}
async function checkBaseNodes(
baseNodes: string[],
extractPatchNotes: boolean,
checkDelay: number,
): Promise<BaseNodeResult[]> {
const results: BaseNodeResult[] = [];
try {
// Fetch n8n releases from GitHub API
const releases = await fetchGitHubAPI(
'https://api.github.com/repos/n8n-io/n8n/releases?per_page=10'
);
await sleep(checkDelay);
// Fetch recent commits from GitHub API
const commits = await fetchGitHubAPI(
'https://api.github.com/repos/n8n-io/n8n/commits?path=packages/nodes-base/nodes&per_page=30'
);
// Combine release notes and commit messages for searching
const releasesContent = releases.map((r: any) =>
`${r.name || ''} ${r.body || ''}`
).join('\n').toLowerCase();
const commitsContent = commits.map((c: any) =>
c.commit?.message || ''
).join('\n').toLowerCase();
// Check each base node
for (const nodeType of [...new Set(baseNodes)]) {
const nodeName = nodeType.split('.').pop() || nodeType;
const lowerNodeName = nodeName.toLowerCase();
const mentionedInReleases = releasesContent.includes(lowerNodeName);
const mentionedInCommits = commitsContent.includes(lowerNodeName);
let patchNotes: string | undefined;
if (extractPatchNotes && (mentionedInReleases || mentionedInCommits)) {
patchNotes = extractPatchNotesFromGitHub(
releases,
commits,
nodeName,
);
}
results.push({
type: 'base',
nodeType,
nodeName,
mentionedInRecentReleases: mentionedInReleases,
mentionedInRecentCommits: mentionedInCommits,
patchNotes,
githubReleasesUrl: 'https://github.com/n8n-io/n8n/releases',
githubCommitsUrl: 'https://github.com/n8n-io/n8n/commits/master/packages/nodes-base/nodes',
success: true,
});
}
return results;
} catch (error) {
throw new Error(`Failed to check base nodes: ${error instanceof Error ? error.message : String(error)}`);
}
}
function extractPatchNotesFromGitHub(
releases: any[],
commits: any[],
nodeName: string,
): string {
const notes: string[] = [];
const lowerNodeName = nodeName.toLowerCase();
// Extract from releases
for (const release of releases) {
const releaseText = `${release.name || ''} ${release.body || ''}`;
if (releaseText.toLowerCase().includes(lowerNodeName)) {
const snippet = extractContextSnippet(releaseText, nodeName, 300);
if (snippet) {
notes.push(`**Release ${release.name || release.tag_name}:**\n${snippet}`);
}
if (notes.length >= 2) break; // Limit to 2 releases
}
}
// Extract from commits
let commitCount = 0;
for (const commit of commits) {
const message = commit.commit?.message || '';
if (message.toLowerCase().includes(lowerNodeName)) {
notes.push(`**Commit:** ${message.split('\n')[0]}`);
commitCount++;
if (commitCount >= 3) break; // Limit to 3 commits
}
}
return notes.join('\n\n') || 'Mentioned in recent updates';
}
function extractContextSnippet(content: string, searchTerm: string, maxLength: number = 200): string {
const lowerContent = content.toLowerCase();
const lowerTerm = searchTerm.toLowerCase();
const index = lowerContent.indexOf(lowerTerm);
if (index === -1) return '';
const start = Math.max(0, index - 100);
const end = Math.min(content.length, index + maxLength);
return '...' + content.substring(start, end).trim() + '...';
}
function generateEmailText(
communityResults: CommunityNodeResult[],
baseResults: BaseNodeResult[],
workflowName: string,
): string {
const lines: string[] = [];
lines.push(`# Node Update Report for: ${workflowName}`);
lines.push(`Generated: ${new Date().toLocaleString()}`);
lines.push('');
lines.push('---');
lines.push('');
// Community Nodes Section
const updatedCommunity = communityResults.filter(r => r.hasUpdate && r.success);
if (updatedCommunity.length > 0) {
lines.push('## 📦 Community Nodes with Updates');
lines.push('');
for (const node of updatedCommunity) {
lines.push(`### ${node.packageName}`);
lines.push(`- **Version Change**: ${node.previousVersion || 'unknown'}${node.currentVersion}`);
lines.push(`- **npm**: ${node.npmUrl}`);
if (node.githubUrl) {
lines.push(`- **GitHub**: ${node.githubUrl}`);
lines.push(`- **Releases**: ${node.githubUrl}/releases`);
}
lines.push(`- **Last Published**: ${node.lastPublished}`);
if (node.changelog) {
lines.push('');
lines.push('**Changelog:**');
lines.push('```');
lines.push(node.changelog);
lines.push('```');
}
lines.push('');
}
} else {
lines.push('## 📦 Community Nodes');
lines.push('No updates detected for community nodes.');
lines.push('');
}
// Base Nodes Section
const updatedBase = baseResults.filter(
r => (r.mentionedInRecentReleases || r.mentionedInRecentCommits) && r.success,
);
if (updatedBase.length > 0) {
lines.push('## 🔧 Base Nodes with Recent Activity');
lines.push('');
for (const node of updatedBase) {
lines.push(`### ${node.nodeName}`);
if (node.mentionedInRecentReleases) {
lines.push('- ✅ Mentioned in recent releases');
}
if (node.mentionedInRecentCommits) {
lines.push('- ✅ Mentioned in recent commits');
}
lines.push(`- **GitHub Releases**: ${node.githubReleasesUrl}`);
lines.push(`- **Commits**: ${node.githubCommitsUrl}`);
if (node.patchNotes) {
lines.push('');
lines.push('**Patch Notes:**');
lines.push('```');
lines.push(node.patchNotes);
lines.push('```');
}
lines.push('');
}
} else {
lines.push('## 🔧 Base Nodes');
lines.push('No recent activity detected for base nodes.');
lines.push('');
}
// No Updates Section
const noUpdatesCommunity = communityResults.filter(r => !r.hasUpdate && r.success);
const noUpdatesBase = baseResults.filter(
r => !r.mentionedInRecentReleases && !r.mentionedInRecentCommits && r.success,
);
if (noUpdatesCommunity.length > 0 || noUpdatesBase.length > 0) {
lines.push('---');
lines.push('## Nodes Without Updates');
lines.push('');
if (noUpdatesCommunity.length > 0) {
lines.push('**Community Nodes:**');
for (const node of noUpdatesCommunity) {
lines.push(`- ${node.packageName} (${node.currentVersion})`);
}
lines.push('');
}
if (noUpdatesBase.length > 0) {
lines.push('**Base Nodes:**');
for (const node of noUpdatesBase) {
lines.push(`- ${node.nodeName}`);
}
lines.push('');
}
}
lines.push('---');
lines.push('');
lines.push('*This report was automatically generated by n8n Workflow Node Checker*');
return lines.join('\n');
}

14541
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,52 +1,60 @@
{ {
"name": "n8n-nodes-<...>", "name": "n8n-nodes-workflow-checker",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "n8n node to check updates for community and base nodes in your workflow",
"license": "MIT", "license": "MIT",
"homepage": "", "homepage": "",
"keywords": [ "keywords": [
"n8n-community-node-package" "n8n-community-node-package",
], "n8n",
"author": { "workflow",
"name": "", "checker",
"email": "" "updates",
}, "npm",
"repository": { "github"
"type": "git", ],
"url": "https://github.com/<...>/n8n-nodes-<...>.git" "author": {
}, "name": "",
"scripts": { "email": ""
"build": "n8n-node build", },
"build:watch": "tsc --watch", "repository": {
"dev": "n8n-node dev", "type": "git",
"lint": "n8n-node lint", "url": ""
"lint:fix": "n8n-node lint --fix", },
"release": "n8n-node release", "scripts": {
"prepublishOnly": "n8n-node prerelease" "build": "n8n-node build",
}, "build:watch": "tsc --watch",
"files": [ "dev": "n8n-node dev",
"dist" "lint": "n8n-node lint",
], "lint:fix": "n8n-node lint --fix",
"n8n": { "release": "n8n-node release",
"n8nNodesApiVersion": 1, "prepublishOnly": "n8n-node prerelease"
"strict": true, },
"credentials": [ "files": [
"dist/credentials/GithubIssuesApi.credentials.js", "dist"
"dist/credentials/GithubIssuesOAuth2Api.credentials.js" ],
], "n8n": {
"nodes": [ "n8nNodesApiVersion": 1,
"dist/nodes/GithubIssues/GithubIssues.node.js", "strict": true,
"dist/nodes/Example/Example.node.js" "credentials": [
] "dist/credentials/FirecrawlApi.credentials.js"
}, ],
"devDependencies": { "nodes": [
"@n8n/node-cli": "*", "dist/nodes/WorkflowNodeChecker/WorkflowNodeChecker.node.js"
"eslint": "9.32.0", ]
"prettier": "3.6.2", },
"release-it": "^19.0.4", "devDependencies": {
"typescript": "5.9.2" "@n8n/node-cli": "*",
}, "@types/node": "^24.10.0",
"peerDependencies": { "eslint": "9.32.0",
"n8n-workflow": "*" "prettier": "3.6.2",
} "release-it": "^19.0.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"n8n-workflow": "*"
},
"dependencies": {
"@mendable/firecrawl-js": "^1.0.0"
}
} }