mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 22:52:16 +03:00
docs: Fix all credential documentation urls, and add a CI job to regularly validate these urls (#5012)
* add or update documentation URLs for all credentials * add UTM params to documentation urls even when they are absolute urls * Setup a CI job to validate documentation urls after every release * Fix FacebookGraphApi documentation URL * also validate node documentation urls Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
0333b053ee
commit
c738aa53e9
41
.github/workflows/check-documentation-urls.yml
vendored
Normal file
41
.github/workflows/check-documentation-urls.yml
vendored
Normal file
@ -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 }})
|
@ -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 (
|
||||
|
@ -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';
|
||||
|
@ -7,6 +7,8 @@ export class BaserowApi implements ICredentialType {
|
||||
|
||||
displayName = 'Baserow API';
|
||||
|
||||
documentationUrl = 'baserow';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Host',
|
||||
|
@ -7,6 +7,8 @@ export class CiscoWebexOAuth2Api implements ICredentialType {
|
||||
|
||||
displayName = 'Cisco Webex OAuth2 API';
|
||||
|
||||
documentationUrl = 'ciscowebex';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
|
@ -10,7 +10,7 @@ export class CitrixAdcApi implements ICredentialType {
|
||||
|
||||
displayName = 'Citrix ADC API';
|
||||
|
||||
documentationUrl = 'citrix';
|
||||
documentationUrl = 'citrixadc';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ export class FacebookGraphApi implements ICredentialType {
|
||||
|
||||
displayName = 'Facebook Graph API';
|
||||
|
||||
documentationUrl = 'facebookGraph';
|
||||
documentationUrl = 'facebookgraph';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ export class FacebookGraphAppApi implements ICredentialType {
|
||||
|
||||
displayName = 'Facebook Graph API (App)';
|
||||
|
||||
documentationUrl = 'facebookGraphApp';
|
||||
documentationUrl = 'facebookapp';
|
||||
|
||||
extends = ['facebookGraphApi'];
|
||||
|
||||
|
@ -7,6 +7,8 @@ export class GetResponseOAuth2Api implements ICredentialType {
|
||||
|
||||
displayName = 'GetResponse OAuth2 API';
|
||||
|
||||
documentationUrl = 'getresponse';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
|
@ -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';
|
||||
|
||||
|
@ -7,6 +7,8 @@ export class HarvestOAuth2Api implements ICredentialType {
|
||||
|
||||
displayName = 'Harvest OAuth2 API';
|
||||
|
||||
documentationUrl = 'harvest';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
|
@ -9,7 +9,7 @@ export class MondayComOAuth2Api implements ICredentialType {
|
||||
|
||||
displayName = 'Monday.com OAuth2 API';
|
||||
|
||||
documentationUrl = 'monday';
|
||||
documentationUrl = 'mondaycom';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ export class SendInBlueApi implements ICredentialType {
|
||||
|
||||
displayName = 'SendInBlue';
|
||||
|
||||
documentationUrl = 'sendInBlueApi';
|
||||
documentationUrl = 'sendinblue';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ export class Smtp implements ICredentialType {
|
||||
|
||||
displayName = 'SMTP';
|
||||
|
||||
documentationUrl = 'smtp';
|
||||
documentationUrl = 'sendemail';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
|
@ -5,6 +5,8 @@ export class SshPassword implements ICredentialType {
|
||||
|
||||
displayName = 'SSH Password';
|
||||
|
||||
documentationUrl = 'ssh';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Host',
|
||||
|
@ -5,6 +5,8 @@ export class SshPrivateKey implements ICredentialType {
|
||||
|
||||
displayName = 'SSH Private Key';
|
||||
|
||||
documentationUrl = 'ssh';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Host',
|
||||
|
@ -10,6 +10,8 @@ export class VenafiTlsProtectCloudApi implements ICredentialType {
|
||||
|
||||
displayName = 'Venafi TLS Protect Cloud';
|
||||
|
||||
documentationUrl = 'venafitlsprotectcloud';
|
||||
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
|
@ -12,6 +12,8 @@ export class VenafiTlsProtectDatacenterApi implements ICredentialType {
|
||||
|
||||
displayName = 'Venafi TLS Protect Datacenter API';
|
||||
|
||||
documentationUrl = 'venafitlsprotectdatacenter';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Domain',
|
||||
|
@ -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": [
|
||||
|
@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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": [
|
||||
|
@ -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": [
|
||||
|
@ -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": [
|
||||
|
@ -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": [
|
||||
|
@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
64
scripts/validate-docs-links.js
Normal file
64
scripts/validate-docs-links.js
Normal file
@ -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');
|
||||
})();
|
Loading…
Reference in New Issue
Block a user