diff --git a/.github/workflows/check-documentation-urls.yml b/.github/workflows/check-documentation-urls.yml new file mode 100644 index 0000000000..f32fc6f165 --- /dev/null +++ b/.github/workflows/check-documentation-urls.yml @@ -0,0 +1,41 @@ +name: Check Documentation URLs + +on: + push: + tags: + - n8n@* + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v3 + + - uses: pnpm/action-setup@v2.2.4 + + - uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build nodes-base + run: pnpm --filter n8n-workflow --filter=n8n-core --filter=n8n-nodes-base build + + - name: Test URLS + run: node scripts/validate-docs-links.js + + - name: Notify Slack on failure + uses: act10ns/slack@v2.0.0 + if: failure() + with: + status: ${{ job.status }} + channel: '#updates-build-alerts' + webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + message: Documentation URLs check failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index fedb9bfdc9..5069b1dbba 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -124,7 +124,7 @@ import OauthButton from './OauthButton.vue'; import { restApi } from '@/mixins/restApi'; import { addCredentialTranslation } from '@/plugins/i18n'; import mixins from 'vue-typed-mixins'; -import { BUILTIN_CREDENTIALS_DOCS_URL, EnterpriseEditionFeature } from '@/constants'; +import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants'; import { IPermissions } from '@/permissions'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; @@ -229,20 +229,29 @@ export default mixins(restApi).extend({ const activeNode = this.ndvStore.activeNode; const isCommunityNode = activeNode ? isCommunityPackageName(activeNode.type) : false; - if (!type || !type.documentationUrl) { + const documentationUrl = type && type.documentationUrl; + + if (!documentationUrl) { return ''; } - if ( - type.documentationUrl.startsWith('https://') || - type.documentationUrl.startsWith('http://') - ) { - return type.documentationUrl; + let url: URL; + if (documentationUrl.startsWith('https://') || documentationUrl.startsWith('http://')) { + url = new URL(documentationUrl); + if (url.hostname !== DOCS_DOMAIN) return documentationUrl; + } else { + // Don't show documentation link for community nodes if the URL is not an absolute path + if (isCommunityNode) return ''; + else url = new URL(`${BUILTIN_CREDENTIALS_DOCS_URL}${documentationUrl}/`); } - return isCommunityNode - ? '' // Don't show documentation link for community nodes if the URL is not an absolute path - : `${BUILTIN_CREDENTIALS_DOCS_URL}${type.documentationUrl}/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal`; + if (url.hostname === DOCS_DOMAIN) { + url.searchParams.set('utm_source', 'n8n_app'); + url.searchParams.set('utm_medium', 'left_nav_menu'); + url.searchParams.set('utm_campaign', 'create_new_credentials_modal'); + } + + return url.href; }, isGoogleOAuthType(): boolean { return ( diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 2cb8775bfe..36bd547d0d 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -57,21 +57,22 @@ export const BREAKPOINT_LG = 1200; export const BREAKPOINT_XL = 1920; export const N8N_IO_BASE_URL = `https://api.n8n.io/api/`; -export const BUILTIN_NODES_DOCS_URL = `https://docs.n8n.io/integrations/builtin/`; -export const BUILTIN_CREDENTIALS_DOCS_URL = `https://docs.n8n.io/integrations/builtin/credentials/`; -export const DATA_PINNING_DOCS_URL = 'https://docs.n8n.io/data/data-pinning/'; -export const DATA_EDITING_DOCS_URL = 'https://docs.n8n.io/data/data-editing/'; +export const DOCS_DOMAIN = 'docs.n8n.io'; +export const BUILTIN_NODES_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/builtin/`; +export const BUILTIN_CREDENTIALS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/builtin/credentials/`; +export const DATA_PINNING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-pinning/`; +export const DATA_EDITING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-editing/`; export const NPM_COMMUNITY_NODE_SEARCH_API_URL = `https://api.npms.io/v2/`; export const NPM_PACKAGE_DOCS_BASE_URL = `https://www.npmjs.com/package/`; export const NPM_KEYWORD_SEARCH_URL = `https://www.npmjs.com/search?q=keywords%3An8n-community-node-package`; -export const N8N_QUEUE_MODE_DOCS_URL = `https://docs.n8n.io/hosting/scaling/queue-mode/`; -export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/installation/`; +export const N8N_QUEUE_MODE_DOCS_URL = `https://${DOCS_DOMAIN}/hosting/scaling/queue-mode/`; +export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/`; export const COMMUNITY_NODES_NPM_INSTALLATION_URL = 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm'; -export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/risks/`; -export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/blocklist/`; -export const CUSTOM_NODES_DOCS_URL = `https://docs.n8n.io/integrations/creating-nodes/code/create-n8n-nodes-module/`; -export const EXPRESSIONS_DOCS_URL = 'https://docs.n8n.io/code-examples/expressions/'; +export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`; +export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/blocklist/`; +export const CUSTOM_NODES_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/creating-nodes/code/create-n8n-nodes-module/`; +export const EXPRESSIONS_DOCS_URL = `https://${DOCS_DOMAIN}/code-examples/expressions/`; // node types export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr'; diff --git a/packages/nodes-base/credentials/BaserowApi.credentials.ts b/packages/nodes-base/credentials/BaserowApi.credentials.ts index 3a944efb83..af833f4ebe 100644 --- a/packages/nodes-base/credentials/BaserowApi.credentials.ts +++ b/packages/nodes-base/credentials/BaserowApi.credentials.ts @@ -7,6 +7,8 @@ export class BaserowApi implements ICredentialType { displayName = 'Baserow API'; + documentationUrl = 'baserow'; + properties: INodeProperties[] = [ { displayName: 'Host', diff --git a/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts b/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts index 37328eb6d9..7deaa1424a 100644 --- a/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts @@ -7,6 +7,8 @@ export class CiscoWebexOAuth2Api implements ICredentialType { displayName = 'Cisco Webex OAuth2 API'; + documentationUrl = 'ciscowebex'; + properties: INodeProperties[] = [ { displayName: 'Grant Type', diff --git a/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts b/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts index 07c5cd8984..ee1daa8dee 100644 --- a/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts +++ b/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts @@ -10,7 +10,7 @@ export class CitrixAdcApi implements ICredentialType { displayName = 'Citrix ADC API'; - documentationUrl = 'citrix'; + documentationUrl = 'citrixadc'; properties: INodeProperties[] = [ { diff --git a/packages/nodes-base/credentials/FacebookGraphApi.credentials.ts b/packages/nodes-base/credentials/FacebookGraphApi.credentials.ts index 2832b72dce..34db599398 100644 --- a/packages/nodes-base/credentials/FacebookGraphApi.credentials.ts +++ b/packages/nodes-base/credentials/FacebookGraphApi.credentials.ts @@ -10,7 +10,7 @@ export class FacebookGraphApi implements ICredentialType { displayName = 'Facebook Graph API'; - documentationUrl = 'facebookGraph'; + documentationUrl = 'facebookgraph'; properties: INodeProperties[] = [ { diff --git a/packages/nodes-base/credentials/FacebookGraphAppApi.credentials.ts b/packages/nodes-base/credentials/FacebookGraphAppApi.credentials.ts index b48b9b3352..ea9aeea30b 100644 --- a/packages/nodes-base/credentials/FacebookGraphAppApi.credentials.ts +++ b/packages/nodes-base/credentials/FacebookGraphAppApi.credentials.ts @@ -5,7 +5,7 @@ export class FacebookGraphAppApi implements ICredentialType { displayName = 'Facebook Graph API (App)'; - documentationUrl = 'facebookGraphApp'; + documentationUrl = 'facebookapp'; extends = ['facebookGraphApi']; diff --git a/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts index e8beb7ef00..4df8c297aa 100644 --- a/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts @@ -7,6 +7,8 @@ export class GetResponseOAuth2Api implements ICredentialType { displayName = 'GetResponse OAuth2 API'; + documentationUrl = 'getresponse'; + properties: INodeProperties[] = [ { displayName: 'Grant Type', diff --git a/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts index 40131946d5..af761ce70b 100644 --- a/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts @@ -7,7 +7,7 @@ export class GoogleOAuth2Api implements ICredentialType { displayName = 'Google OAuth2 API'; - documentationUrl = 'google/oauth-generic/'; + documentationUrl = 'google/oauth-generic'; icon = 'file:Google.svg'; diff --git a/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts b/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts index f8e97421cc..cd25001235 100644 --- a/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts @@ -7,6 +7,8 @@ export class HarvestOAuth2Api implements ICredentialType { displayName = 'Harvest OAuth2 API'; + documentationUrl = 'harvest'; + properties: INodeProperties[] = [ { displayName: 'Grant Type', diff --git a/packages/nodes-base/credentials/MondayComOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MondayComOAuth2Api.credentials.ts index ac1ea8a58c..7c4c6346bd 100644 --- a/packages/nodes-base/credentials/MondayComOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MondayComOAuth2Api.credentials.ts @@ -9,7 +9,7 @@ export class MondayComOAuth2Api implements ICredentialType { displayName = 'Monday.com OAuth2 API'; - documentationUrl = 'monday'; + documentationUrl = 'mondaycom'; properties: INodeProperties[] = [ { diff --git a/packages/nodes-base/credentials/SendInBlueApi.credentials.ts b/packages/nodes-base/credentials/SendInBlueApi.credentials.ts index 921c895c38..bf1dbbe868 100644 --- a/packages/nodes-base/credentials/SendInBlueApi.credentials.ts +++ b/packages/nodes-base/credentials/SendInBlueApi.credentials.ts @@ -10,7 +10,7 @@ export class SendInBlueApi implements ICredentialType { displayName = 'SendInBlue'; - documentationUrl = 'sendInBlueApi'; + documentationUrl = 'sendinblue'; properties: INodeProperties[] = [ { diff --git a/packages/nodes-base/credentials/Smtp.credentials.ts b/packages/nodes-base/credentials/Smtp.credentials.ts index b5b9e8fad0..f600384bde 100644 --- a/packages/nodes-base/credentials/Smtp.credentials.ts +++ b/packages/nodes-base/credentials/Smtp.credentials.ts @@ -5,7 +5,7 @@ export class Smtp implements ICredentialType { displayName = 'SMTP'; - documentationUrl = 'smtp'; + documentationUrl = 'sendemail'; properties: INodeProperties[] = [ { diff --git a/packages/nodes-base/credentials/SshPassword.credentials.ts b/packages/nodes-base/credentials/SshPassword.credentials.ts index 4cbb92e56f..895cf2c2e7 100644 --- a/packages/nodes-base/credentials/SshPassword.credentials.ts +++ b/packages/nodes-base/credentials/SshPassword.credentials.ts @@ -5,6 +5,8 @@ export class SshPassword implements ICredentialType { displayName = 'SSH Password'; + documentationUrl = 'ssh'; + properties: INodeProperties[] = [ { displayName: 'Host', diff --git a/packages/nodes-base/credentials/SshPrivateKey.credentials.ts b/packages/nodes-base/credentials/SshPrivateKey.credentials.ts index 9ec9213ef1..7ed248be1c 100644 --- a/packages/nodes-base/credentials/SshPrivateKey.credentials.ts +++ b/packages/nodes-base/credentials/SshPrivateKey.credentials.ts @@ -5,6 +5,8 @@ export class SshPrivateKey implements ICredentialType { displayName = 'SSH Private Key'; + documentationUrl = 'ssh'; + properties: INodeProperties[] = [ { displayName: 'Host', diff --git a/packages/nodes-base/credentials/VenafiTlsProtectCloudApi.credentials.ts b/packages/nodes-base/credentials/VenafiTlsProtectCloudApi.credentials.ts index 80bce02628..863dd2dd36 100644 --- a/packages/nodes-base/credentials/VenafiTlsProtectCloudApi.credentials.ts +++ b/packages/nodes-base/credentials/VenafiTlsProtectCloudApi.credentials.ts @@ -10,6 +10,8 @@ export class VenafiTlsProtectCloudApi implements ICredentialType { displayName = 'Venafi TLS Protect Cloud'; + documentationUrl = 'venafitlsprotectcloud'; + properties = [ { displayName: 'API Key', diff --git a/packages/nodes-base/credentials/VenafiTlsProtectDatacenterApi.credentials.ts b/packages/nodes-base/credentials/VenafiTlsProtectDatacenterApi.credentials.ts index 6f94b32937..2ea3e508e8 100644 --- a/packages/nodes-base/credentials/VenafiTlsProtectDatacenterApi.credentials.ts +++ b/packages/nodes-base/credentials/VenafiTlsProtectDatacenterApi.credentials.ts @@ -12,6 +12,8 @@ export class VenafiTlsProtectDatacenterApi implements ICredentialType { displayName = 'Venafi TLS Protect Datacenter API'; + documentationUrl = 'venafitlsprotectdatacenter'; + properties: INodeProperties[] = [ { displayName: 'Domain', diff --git a/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.json b/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.json index f1bc023d8e..7915e6966f 100644 --- a/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.json +++ b/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.json @@ -11,7 +11,7 @@ ], "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awsCertificateManager/" + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awscertificatemanager/" } ], "generic": [ diff --git a/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.json b/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.json index 347e577a28..7f2c0eddee 100644 --- a/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.json +++ b/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.json @@ -11,7 +11,7 @@ ], "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awsElb/" + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awselb/" } ] } diff --git a/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json b/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json index 8239059cde..384d4273e0 100644 --- a/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json +++ b/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json @@ -11,7 +11,7 @@ ], "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.citrixAdc/" + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.citrixadc/" } ] } diff --git a/packages/nodes-base/nodes/Cron/Cron.node.json b/packages/nodes-base/nodes/Cron/Cron.node.json index fc761179ee..fe4f3fc061 100644 --- a/packages/nodes-base/nodes/Cron/Cron.node.json +++ b/packages/nodes-base/nodes/Cron/Cron.node.json @@ -7,7 +7,7 @@ "resources": { "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.cron/" + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/" } ], "generic": [ diff --git a/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.json b/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.json index 66db784457..321c472398 100644 --- a/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.json +++ b/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.json @@ -11,7 +11,7 @@ ], "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.imapemail/" + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.emailimap/" } ], "generic": [ diff --git a/packages/nodes-base/nodes/Function/Function.node.json b/packages/nodes-base/nodes/Function/Function.node.json index 0a92eb2363..af66a5047d 100644 --- a/packages/nodes-base/nodes/Function/Function.node.json +++ b/packages/nodes-base/nodes/Function/Function.node.json @@ -7,7 +7,7 @@ "resources": { "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/" + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/" } ], "generic": [ diff --git a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.json b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.json index 600f85c794..aa09a8a2f3 100644 --- a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.json +++ b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.json @@ -6,7 +6,7 @@ "resources": { "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.functionitem/" + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/" } ], "generic": [ diff --git a/packages/nodes-base/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloud.node.json b/packages/nodes-base/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloud.node.json index a1fc4fc0a2..7b3b84853e 100644 --- a/packages/nodes-base/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloud.node.json +++ b/packages/nodes-base/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloud.node.json @@ -11,7 +11,7 @@ ], "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.venafiTlsProtectCloud/" + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.venafitlsprotectcloud/" } ] } diff --git a/scripts/validate-docs-links.js b/scripts/validate-docs-links.js new file mode 100644 index 0000000000..aab9a23380 --- /dev/null +++ b/scripts/validate-docs-links.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +const path = require('path'); +const https = require('https'); +const glob = require('fast-glob'); +const pLimit = require('p-limit'); + +const nodesBaseDir = path.resolve(__dirname, '../packages/nodes-base'); + +const validateUrl = async (kind, name, documentationUrl) => + new Promise((resolve, reject) => { + if (!documentationUrl) resolve([name, null]); + const url = new URL( + /^https?:\/\//.test(documentationUrl) + ? documentationUrl + : `https://docs.n8n.io/integrations/builtin/${kind}/${documentationUrl.toLowerCase()}/`, + ); + https + .request( + { + hostname: url.hostname, + port: 443, + path: url.pathname, + method: 'HEAD', + }, + (res) => resolve([name, res.statusCode]), + ) + .on('error', (e) => reject(e)) + .end(); + }); + +const checkLinks = async (kind) => { + let types = require(path.join(nodesBaseDir, `dist/types/${kind}.json`)); + if (kind === 'nodes') + types = types.filter(({ codex }) => !!codex?.resources?.primaryDocumentation); + const limit = pLimit(30); + const statuses = await Promise.all( + types.map((type) => + limit(() => { + const documentationUrl = + kind === 'credentials' + ? type.documentationUrl + : type.codex?.resources?.primaryDocumentation?.[0]?.url; + return validateUrl(kind, type.displayName, documentationUrl); + }), + ), + ); + + const missingDocs = []; + const invalidUrls = []; + for (const [name, statusCode] of statuses) { + if (statusCode === null) missingDocs.push(name); + if (statusCode !== 200) invalidUrls.push(name); + } + + if (missingDocs.length) console.log('Documentation URL missing for %s', kind, missingDocs); + if (invalidUrls.length) console.log('Documentation URL invalid for %s', kind, invalidUrls); + if (missingDocs.length || invalidUrls.length) process.exit(1); +}; + +(async () => { + await checkLinks('credentials'); + await checkLinks('nodes'); +})();