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:
कारतोफ्फेलस्क्रिप्ट™ 2022-12-22 17:01:29 +01:00 committed by GitHub
parent 0333b053ee
commit c738aa53e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 166 additions and 35 deletions

View 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 }})

View file

@ -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 (

View file

@ -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';

View file

@ -7,6 +7,8 @@ export class BaserowApi implements ICredentialType {
displayName = 'Baserow API';
documentationUrl = 'baserow';
properties: INodeProperties[] = [
{
displayName: 'Host',

View file

@ -7,6 +7,8 @@ export class CiscoWebexOAuth2Api implements ICredentialType {
displayName = 'Cisco Webex OAuth2 API';
documentationUrl = 'ciscowebex';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',

View file

@ -10,7 +10,7 @@ export class CitrixAdcApi implements ICredentialType {
displayName = 'Citrix ADC API';
documentationUrl = 'citrix';
documentationUrl = 'citrixadc';
properties: INodeProperties[] = [
{

View file

@ -10,7 +10,7 @@ export class FacebookGraphApi implements ICredentialType {
displayName = 'Facebook Graph API';
documentationUrl = 'facebookGraph';
documentationUrl = 'facebookgraph';
properties: INodeProperties[] = [
{

View file

@ -5,7 +5,7 @@ export class FacebookGraphAppApi implements ICredentialType {
displayName = 'Facebook Graph API (App)';
documentationUrl = 'facebookGraphApp';
documentationUrl = 'facebookapp';
extends = ['facebookGraphApi'];

View file

@ -7,6 +7,8 @@ export class GetResponseOAuth2Api implements ICredentialType {
displayName = 'GetResponse OAuth2 API';
documentationUrl = 'getresponse';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',

View file

@ -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';

View file

@ -7,6 +7,8 @@ export class HarvestOAuth2Api implements ICredentialType {
displayName = 'Harvest OAuth2 API';
documentationUrl = 'harvest';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',

View file

@ -9,7 +9,7 @@ export class MondayComOAuth2Api implements ICredentialType {
displayName = 'Monday.com OAuth2 API';
documentationUrl = 'monday';
documentationUrl = 'mondaycom';
properties: INodeProperties[] = [
{

View file

@ -10,7 +10,7 @@ export class SendInBlueApi implements ICredentialType {
displayName = 'SendInBlue';
documentationUrl = 'sendInBlueApi';
documentationUrl = 'sendinblue';
properties: INodeProperties[] = [
{

View file

@ -5,7 +5,7 @@ export class Smtp implements ICredentialType {
displayName = 'SMTP';
documentationUrl = 'smtp';
documentationUrl = 'sendemail';
properties: INodeProperties[] = [
{

View file

@ -5,6 +5,8 @@ export class SshPassword implements ICredentialType {
displayName = 'SSH Password';
documentationUrl = 'ssh';
properties: INodeProperties[] = [
{
displayName: 'Host',

View file

@ -5,6 +5,8 @@ export class SshPrivateKey implements ICredentialType {
displayName = 'SSH Private Key';
documentationUrl = 'ssh';
properties: INodeProperties[] = [
{
displayName: 'Host',

View file

@ -10,6 +10,8 @@ export class VenafiTlsProtectCloudApi implements ICredentialType {
displayName = 'Venafi TLS Protect Cloud';
documentationUrl = 'venafitlsprotectcloud';
properties = [
{
displayName: 'API Key',

View file

@ -12,6 +12,8 @@ export class VenafiTlsProtectDatacenterApi implements ICredentialType {
displayName = 'Venafi TLS Protect Datacenter API';
documentationUrl = 'venafitlsprotectdatacenter';
properties: INodeProperties[] = [
{
displayName: 'Domain',

View file

@ -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": [

View file

@ -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/"
}
]
}

View file

@ -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/"
}
]
}

View file

@ -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": [

View file

@ -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": [

View file

@ -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": [

View file

@ -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": [

View file

@ -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/"
}
]
}

View 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');
})();