first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,235 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { matchGlob, parseFilters, evaluateFilter, runValidate } from '../ci-filter.mjs';
// --- matchGlob ---
describe('matchGlob', () => {
it('** matches dotfiles', () => {
assert.ok(matchGlob('.github/workflows/ci.yml', '**'));
});
it('** matches deeply nested paths', () => {
assert.ok(matchGlob('packages/cli/src/controllers/auth.ts', '**'));
});
it('** matches root-level files', () => {
assert.ok(matchGlob('README.md', '**'));
});
it('.github/** matches workflow files', () => {
assert.ok(matchGlob('.github/workflows/ci.yml', '.github/**'));
});
it('.github/** matches action files', () => {
assert.ok(matchGlob('.github/actions/ci-filter/action.yml', '.github/**'));
});
it('.github/** does not match non-.github paths', () => {
assert.ok(!matchGlob('packages/cli/src/index.ts', '.github/**'));
});
it('scoped package pattern matches files in that package', () => {
assert.ok(
matchGlob(
'packages/@n8n/task-runner-python/src/main.py',
'packages/@n8n/task-runner-python/**',
),
);
});
it('scoped package pattern does not match other packages', () => {
assert.ok(!matchGlob('packages/@n8n/config/src/index.ts', 'packages/@n8n/task-runner-python/**'));
});
it('* matches single-level only', () => {
assert.ok(matchGlob('README.md', '*.md'));
assert.ok(!matchGlob('docs/README.md', '*.md'));
});
it('exact path match', () => {
assert.ok(matchGlob('package.json', 'package.json'));
assert.ok(!matchGlob('packages/cli/package.json', 'package.json'));
});
it('? matches single character', () => {
assert.ok(matchGlob('file1.txt', 'file?.txt'));
assert.ok(!matchGlob('file12.txt', 'file?.txt'));
});
it('**/ at start matches zero or more path segments', () => {
assert.ok(matchGlob('src/index.ts', '**/index.ts'));
assert.ok(matchGlob('packages/cli/src/index.ts', '**/index.ts'));
assert.ok(matchGlob('index.ts', '**/index.ts'));
});
it('**/ in middle matches nested paths', () => {
assert.ok(matchGlob('packages/@n8n/db/src/deep/file.ts', 'packages/@n8n/db/**'));
});
});
// --- parseFilters ---
describe('parseFilters', () => {
it('parses single-line filter', () => {
const filters = parseFilters('workflows: .github/**');
assert.deepEqual(filters.get('workflows'), ['.github/**']);
});
it('parses single-line with multiple patterns', () => {
const filters = parseFilters('db: packages/@n8n/db/** packages/cli/**');
assert.deepEqual(filters.get('db'), ['packages/@n8n/db/**', 'packages/cli/**']);
});
it('parses multi-line filter', () => {
const input = `non-python:
**
!packages/@n8n/task-runner-python/**`;
const filters = parseFilters(input);
assert.deepEqual(filters.get('non-python'), ['**', '!packages/@n8n/task-runner-python/**']);
});
it('parses mixed single and multi-line', () => {
const input = `non-python:
**
!packages/@n8n/task-runner-python/**
workflows: .github/**`;
const filters = parseFilters(input);
assert.equal(filters.size, 2);
assert.deepEqual(filters.get('non-python'), ['**', '!packages/@n8n/task-runner-python/**']);
assert.deepEqual(filters.get('workflows'), ['.github/**']);
});
it('ignores comments and blank lines', () => {
const input = `# This is a comment
workflows: .github/**
# Another comment
db: packages/@n8n/db/**`;
const filters = parseFilters(input);
assert.equal(filters.size, 2);
});
it('throws on malformed input', () => {
assert.throws(() => parseFilters('not a valid filter line'), /Malformed/);
});
it('throws on filter with no patterns', () => {
const input = `empty:
other: .github/**`;
assert.throws(() => parseFilters(input), /no patterns/);
});
});
// --- evaluateFilter ---
describe('evaluateFilter', () => {
it('python-only files with non-python filter returns false', () => {
const files = [
'packages/@n8n/task-runner-python/src/main.py',
'packages/@n8n/task-runner-python/pyproject.toml',
];
const patterns = ['**', '!packages/@n8n/task-runner-python/**'];
assert.equal(evaluateFilter(files, patterns), false);
});
it('mixed python and non-python returns true', () => {
const files = [
'packages/@n8n/task-runner-python/src/main.py',
'packages/cli/src/index.ts',
];
const patterns = ['**', '!packages/@n8n/task-runner-python/**'];
assert.equal(evaluateFilter(files, patterns), true);
});
it('non-python files with non-python filter returns true', () => {
const files = ['packages/cli/src/index.ts', 'packages/core/src/utils.ts'];
const patterns = ['**', '!packages/@n8n/task-runner-python/**'];
assert.equal(evaluateFilter(files, patterns), true);
});
it('.github files with workflows filter returns true', () => {
const files = ['.github/workflows/ci.yml', '.github/actions/setup/action.yml'];
const patterns = ['.github/**'];
assert.equal(evaluateFilter(files, patterns), true);
});
it('non-.github files with workflows filter returns false', () => {
const files = ['packages/cli/src/index.ts'];
const patterns = ['.github/**'];
assert.equal(evaluateFilter(files, patterns), false);
});
it('empty changed files returns false', () => {
assert.equal(evaluateFilter([], ['**']), false);
});
it('last matching pattern wins (gitignore semantics)', () => {
const files = ['packages/@n8n/task-runner-python/src/main.py'];
const patterns = ['**', '!packages/@n8n/task-runner-python/**', 'packages/@n8n/task-runner-python/**'];
assert.equal(evaluateFilter(files, patterns), true);
});
});
// --- runValidate ---
describe('runValidate', () => {
function runWithResults(jobResults: Record<string, { result: string }>): number | null {
const originalEnv = process.env.INPUT_JOB_RESULTS;
const originalExit = process.exit;
let exitCode: number | null = null;
process.env.INPUT_JOB_RESULTS = JSON.stringify(jobResults);
process.exit = ((code: number) => { exitCode = code; }) as never;
try {
runValidate();
} finally {
process.env.INPUT_JOB_RESULTS = originalEnv;
process.exit = originalExit;
}
return exitCode;
}
it('passes when all jobs succeed', () => {
assert.equal(runWithResults({
'install-and-build': { result: 'success' },
'unit-test': { result: 'success' },
typecheck: { result: 'success' },
lint: { result: 'success' },
}), null);
});
it('passes when some jobs are skipped (filtered out)', () => {
assert.equal(runWithResults({
'install-and-build': { result: 'success' },
'unit-test': { result: 'success' },
'security-checks': { result: 'skipped' },
}), null);
});
it('fails when a job fails', () => {
assert.equal(runWithResults({
'install-and-build': { result: 'success' },
'unit-test': { result: 'failure' },
typecheck: { result: 'success' },
}), 1);
});
it('fails when a job is cancelled', () => {
assert.equal(runWithResults({
'install-and-build': { result: 'success' },
'unit-test': { result: 'cancelled' },
}), 1);
});
it('fails when multiple jobs have problems', () => {
assert.equal(runWithResults({
'unit-test': { result: 'failure' },
typecheck: { result: 'cancelled' },
lint: { result: 'success' },
}), 1);
});
});
+39
View File
@@ -0,0 +1,39 @@
name: 'CI Filter'
description: |
Filter CI jobs by changed files and validate results.
Modes:
- filter: Determines which jobs to run based on changed files and a provided filter definition.
- validate: Checks the results of required jobs and fails if any of them failed or were cancelled.
inputs:
mode:
description: 'filter or validate'
required: true
filters:
description: 'Filter definitions (gitignore-style DSL)'
required: false
base-ref:
description: 'Base branch for diff. Auto-detected if not specified.'
required: false
job-results:
description: 'Job results from needs context as JSON (mode=validate)'
required: false
outputs:
results:
description: 'JSON object: { "filter-name": true/false }'
value: ${{ steps.run.outputs.results }}
runs:
using: 'composite'
steps:
- name: Run CI Filter
id: run
shell: bash
env:
INPUT_MODE: ${{ inputs.mode }}
INPUT_FILTERS: ${{ inputs.filters }}
INPUT_BASE_REF: ${{ inputs.base-ref || github.event.pull_request.base.ref || github.event.merge_group.base_ref || 'master' }}
INPUT_JOB_RESULTS: ${{ inputs.job-results }}
run: node ${{ github.action_path }}/ci-filter.mjs
+216
View File
@@ -0,0 +1,216 @@
import { execSync } from 'node:child_process';
import { appendFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
// --- Glob matching (dotfile-safe) ---
/**
* Match a file path against a glob pattern.
* Unlike path.matchesGlob / standard POSIX globs, `**` matches dotfiles.
*/
export function matchGlob(filePath, pattern) {
let regex = '';
let i = 0;
while (i < pattern.length) {
const ch = pattern[i];
if (ch === '*' && pattern[i + 1] === '*') {
if (pattern[i + 2] === '/') {
regex += '(?:.+/)?';
i += 3;
} else {
regex += '.*';
i += 2;
}
} else if (ch === '*') {
regex += '[^/]*';
i++;
} else if (ch === '?') {
regex += '[^/]';
i++;
} else {
regex += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
i++;
}
}
return new RegExp(`^${regex}$`).test(filePath);
}
// --- Filter DSL parser ---
/**
* Parse filter definitions from the input DSL.
*
* Supports two formats:
* Single-line: `name: pattern1 pattern2`
* Multi-line: `name:` followed by indented patterns (one per line)
*
* Lines starting with # and blank lines are ignored.
*/
export function parseFilters(input) {
const filters = new Map();
const lines = input.split('\n');
let currentFilter = null;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const headerMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)?$/);
if (headerMatch) {
const name = headerMatch[1];
const rest = (headerMatch[2] || '').trim();
const patterns = [];
currentFilter = name;
filters.set(name, patterns);
if (rest) {
patterns.push(...rest.split(/\s+/));
currentFilter = null;
}
continue;
}
if (currentFilter && rawLine.match(/^\s/)) {
const patterns = filters.get(currentFilter);
if (patterns) patterns.push(line);
continue;
}
throw new Error(`Malformed filter input at: "${rawLine}"`);
}
for (const [name, patterns] of filters) {
if (patterns.length === 0) {
throw new Error(`Filter "${name}" has no patterns`);
}
}
return filters;
}
// --- Git operations ---
const SAFE_REF = /^[a-zA-Z0-9_./-]+$/;
export function getChangedFiles(baseRef) {
if (!SAFE_REF.test(baseRef)) {
throw new Error(`Unsafe base ref: "${baseRef}"`);
}
execSync(`git fetch --depth=1 origin ${baseRef}`, { stdio: 'pipe' });
const output = execSync('git diff --name-only FETCH_HEAD HEAD', { encoding: 'utf-8' });
return output
.split('\n')
.map((f) => f.trim())
.filter(Boolean);
}
// --- Filter evaluation ---
/**
* Evaluate a single filter against changed files using gitignore semantics.
* Patterns evaluated in order, last match wins. ! prefix excludes.
* Filter triggers if ANY changed file passes.
*/
export function evaluateFilter(changedFiles, patterns) {
for (const file of changedFiles) {
let included = false;
for (const pattern of patterns) {
if (pattern.startsWith('!')) {
if (matchGlob(file, pattern.slice(1))) {
included = false;
}
} else {
if (matchGlob(file, pattern)) {
included = true;
}
}
}
if (included) return true;
}
return false;
}
// --- Mode: filter ---
function setOutput(name, value) {
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
const delimiter = `ghadelimiter_${Date.now()}`;
appendFileSync(outputFile, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
}
}
export function runFilter() {
const filtersInput = process.env.INPUT_FILTERS;
const baseRef = process.env.INPUT_BASE_REF;
if (!filtersInput) {
throw new Error('INPUT_FILTERS is required in filter mode');
}
if (!baseRef) {
throw new Error('INPUT_BASE_REF is required in filter mode');
}
const filters = parseFilters(filtersInput);
const changedFiles = getChangedFiles(baseRef);
console.log(`Changed files (${changedFiles.length}):`);
for (const f of changedFiles) {
console.log(` ${f}`);
}
const results = {};
for (const [name, patterns] of filters) {
const matched = evaluateFilter(changedFiles, patterns);
results[name] = matched;
console.log(`Filter "${name}": ${matched}`);
}
setOutput('results', JSON.stringify(results));
}
// --- Mode: validate ---
export function runValidate() {
const raw = process.env.INPUT_JOB_RESULTS;
if (!raw) {
throw new Error('INPUT_JOB_RESULTS is required in validate mode');
}
const jobResults = JSON.parse(raw);
const problems = [];
for (const [job, data] of Object.entries(jobResults)) {
if (data.result === 'failure') problems.push(`${job}: failed`);
if (data.result === 'cancelled') problems.push(`${job}: cancelled`);
}
if (problems.length > 0) {
console.error('Required checks failed:');
for (const p of problems) {
console.error(` - ${p}`);
}
process.exit(1);
}
console.log('All required checks passed:');
for (const [job, data] of Object.entries(jobResults)) {
console.log(` ${job}: ${data.result}`);
}
}
// --- Main (only when run directly, not when imported by tests) ---
if (resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1])) {
const mode = process.env.INPUT_MODE;
if (mode === 'filter') {
runFilter();
} else if (mode === 'validate') {
runValidate();
} else {
throw new Error(`Unknown mode: "${mode}". Expected "filter" or "validate".`);
}
}
@@ -0,0 +1,51 @@
# Composite action for logging into Docker registries (GHCR and/or DockerHub).
# Centralizes the login pattern used across multiple Docker workflows.
name: 'Docker Registry Login'
description: 'Login to GitHub Container Registry and/or DockerHub'
inputs:
login-ghcr:
description: 'Login to GitHub Container Registry'
required: false
default: 'true'
login-dockerhub:
description: 'Login to DockerHub'
required: false
default: 'false'
login-dhi:
description: 'Login to Docker Hardened Images registry (dhi.io)'
required: false
default: 'false'
dockerhub-username:
description: 'DockerHub username (required if login-dockerhub or login-dhi is true)'
required: false
dockerhub-password:
description: 'DockerHub password (required if login-dockerhub or login-dhi is true)'
required: false
runs:
using: 'composite'
steps:
- name: Login to GitHub Container Registry
if: inputs.login-ghcr == 'true'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Login to DockerHub
if: inputs.login-dockerhub == 'true'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-password }}
- name: Login to DHI Registry
if: inputs.login-dhi == 'true'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: dhi.io
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-password }}
+78
View File
@@ -0,0 +1,78 @@
# This action works transparently on both Blacksmith and GitHub-hosted runners.
# Blacksmith runners benefit from transparent caching and optional Docker layer caching.
# GitHub-hosted runners use standard GitHub Actions caching.
name: 'Node.js Build Setup'
description: 'Configures Node.js with pnpm, installs Aikido SafeChain for supply chain protection, installs dependencies, enables Turborepo caching, (optional) sets up Docker layer caching, and builds the project or an optional command.'
inputs:
node-version:
description: 'Node.js version to use. Pinned to 24.13.1 by default for reproducible builds.'
required: false
default: '24.13.1'
enable-docker-cache:
description: 'Whether to set up Blacksmith Buildx for Docker layer caching (Blacksmith runners only).'
required: false
default: 'false'
build-command:
description: 'Command to execute for building the project or an optional command. Leave empty to skip build step.'
required: false
default: 'pnpm build'
install-command:
description: 'Command to execute for installing project dependencies. Leave empty to skip install step.'
required: false
default: 'pnpm install --frozen-lockfile'
runs:
using: 'composite'
steps:
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: ${{ inputs.node-version }}
cache: 'pnpm'
- name: Install Aikido SafeChain
if: runner.os != 'Windows'
run: |
VERSION="1.4.1"
EXPECTED_SHA256="628235987175072a4255aa3f5f0128f31795b63970f1970ae8a04d07bf8527b0"
curl -fsSL -o install-safe-chain.sh \
"https://github.com/AikidoSec/safe-chain/releases/download/${VERSION}/install-safe-chain.sh"
echo "${EXPECTED_SHA256} install-safe-chain.sh" | sha256sum -c -
sh install-safe-chain.sh --ci
rm install-safe-chain.sh
shell: bash
- name: Install Dependencies
if: ${{ inputs.install-command != '' }}
run: |
${{ inputs.install-command }}
shell: bash
- name: Disable safe-chain
if: runner.os != 'Windows'
run: safe-chain teardown
shell: bash
- name: Configure Turborepo Cache
uses: rharkor/caching-for-turbo@cda201ff2b32699a2ae9f59704db029e3dde4fbd # v2.3.5
- name: Setup Docker Builder for Docker Cache (Blacksmith)
if: ${{ inputs.enable-docker-cache == 'true' && vars.RUNNER_PROVIDER != 'github' }}
uses: useblacksmith/setup-docker-builder@53647ab5afe8827af5623b35bd4302eabd41619f # v1.2.0
- name: Setup Docker Builder (GitHub fallback)
if: ${{ inputs.enable-docker-cache == 'true' && vars.RUNNER_PROVIDER == 'github' }}
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Build Project
if: ${{ inputs.build-command != '' }}
run: |
${{ inputs.build-command }} --summarize
node .github/scripts/send-build-stats.mjs || true
node .github/scripts/send-docker-stats.mjs || true
shell: bash