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
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user