blob: 8fe50c889ec71a93d3d19a4811795e3517598c9c [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# .github/workflows/_detect.yml
name: _detect
on:
workflow_call:
outputs:
rust_matrix:
description: "Matrix for Rust components"
value: ${{ jobs.detect.outputs.rust_matrix }}
python_matrix:
description: "Matrix for Python SDK"
value: ${{ jobs.detect.outputs.python_matrix }}
node_matrix:
description: "Matrix for Node SDK"
value: ${{ jobs.detect.outputs.node_matrix }}
go_matrix:
description: "Matrix for Go SDK"
value: ${{ jobs.detect.outputs.go_matrix }}
java_matrix:
description: "Matrix for Java SDK"
value: ${{ jobs.detect.outputs.java_matrix }}
csharp_matrix:
description: "Matrix for C# SDK"
value: ${{ jobs.detect.outputs.csharp_matrix }}
bdd_matrix:
description: "Matrix for BDD tests"
value: ${{ jobs.detect.outputs.bdd_matrix }}
examples_matrix:
description: "Matrix for examples tests"
value: ${{ jobs.detect.outputs.examples_matrix }}
other_matrix:
description: "Matrix for other components"
value: ${{ jobs.detect.outputs.other_matrix }}
jobs:
detect:
runs-on: ubuntu-latest
outputs:
rust_matrix: ${{ steps.mk.outputs.rust_matrix }}
python_matrix: ${{ steps.mk.outputs.python_matrix }}
node_matrix: ${{ steps.mk.outputs.node_matrix }}
go_matrix: ${{ steps.mk.outputs.go_matrix }}
java_matrix: ${{ steps.mk.outputs.java_matrix }}
csharp_matrix: ${{ steps.mk.outputs.csharp_matrix }}
bdd_matrix: ${{ steps.mk.outputs.bdd_matrix }}
examples_matrix: ${{ steps.mk.outputs.examples_matrix }}
other_matrix: ${{ steps.mk.outputs.other_matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files (PR or push)
id: changed
uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c
with:
format: json
- name: Load components config
id: config
run: |
if ! command -v yq &> /dev/null; then
YQ_VERSION="v4.47.1"
YQ_CHECKSUM="0fb28c6680193c41b364193d0c0fc4a03177aecde51cfc04d506b1517158c2fb"
wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64
echo "${YQ_CHECKSUM} /tmp/yq" | sha256sum -c - || exit 1
chmod +x /tmp/yq
sudo mv /tmp/yq /usr/local/bin/yq
fi
echo "components=$(yq -o=json -I=0 '.' .github/config/components.yml | jq -c)" >> $GITHUB_OUTPUT
- name: Build matrices
id: mk
uses: actions/github-script@v7
with:
script: |
const componentsJson = `${{ steps.config.outputs.components }}`;
const changedFilesJson = `${{ steps.changed.outputs.all || '[]' }}`;
let componentsCfg = { components: {} };
try { componentsCfg = JSON.parse(componentsJson); } catch {}
const components = componentsCfg.components || {};
let changedFiles = [];
try { changedFiles = JSON.parse(changedFilesJson); } catch {}
const files = changedFiles.map(p => p.replace(/\\/g, '/'));
const escapeRe = s => s.replace(/([.+^${}()|[\]\\])/g, '\\$1');
const globToRegex = (pattern) => {
// Handle exclusion patterns
const isExclusion = pattern.startsWith('!');
const cleanPattern = isExclusion ? pattern.slice(1) : pattern;
let s = escapeRe(cleanPattern);
// First, replace ** patterns with placeholders to protect them
s = s.replace(/\*\*\/+/g, '___DOUBLESTAR_SLASH___'); // '**/' -> placeholder
s = s.replace(/\/\*\*$/g, '___SLASH_DOUBLESTAR___'); // '/**' at end -> placeholder
s = s.replace(/\*\*/g, '___DOUBLESTAR___'); // remaining '**' -> placeholder
// Now handle single * and ? (they won't match our placeholders)
s = s.replace(/\*/g, '[^/]*'); // '*' -> match any except /
s = s.replace(/\?/g, '[^/]'); // '?' -> match single char except /
// Finally, replace placeholders with actual regex patterns
s = s.replace(/___DOUBLESTAR_SLASH___/g, '(?:.*/)?'); // '**/' -> any subpath (optional)
s = s.replace(/___SLASH_DOUBLESTAR___/g, '(?:/.*)?'); // '/**' at end -> rest of path
s = s.replace(/___DOUBLESTAR___/g, '.*'); // remaining '**' -> match anything
return { regex: new RegExp(`^${s}$`), exclude: isExclusion };
};
const compiled = new Map();
const toRx = (p) => {
if (!compiled.has(p)) compiled.set(p, globToRegex(p));
return compiled.get(p).regex;
};
const test = (file, patterns) => {
const inc = patterns.filter(p => !p.startsWith('!')).map(toRx);
const exc = patterns.filter(p => p.startsWith('!')).map(p => toRx(p.slice(1)));
const included = inc.some(rx => rx.test(file));
const excluded = exc.some(rx => rx.test(file));
return included && !excluded;
};
// Build dependency graph and resolve affected components
const affectedComponents = new Set();
const componentTriggers = new Map(); // Track what triggered each component
const dependencyGraph = new Map();
// First pass: build dependency graph
for (const [name, cfg] of Object.entries(components)) {
if (cfg.depends_on) {
dependencyGraph.set(name, cfg.depends_on);
}
}
// Second pass: check which components are directly affected by file changes
console.log('=== Change Detection ===');
console.log(`Analyzing ${files.length} changed files...`);
console.log('');
for (const [name, cfg] of Object.entries(components)) {
const pats = (cfg.paths || []);
const matchedFiles = files.filter(f => test(f, pats));
if (matchedFiles.length > 0) {
affectedComponents.add(name);
componentTriggers.set(name, {
reason: 'direct',
files: matchedFiles,
dependency: null
});
// Log direct matches
console.log(`✓ ${name} directly affected by:`);
if (matchedFiles.length <= 5) {
matchedFiles.forEach(f => console.log(` - ${f}`));
} else {
matchedFiles.slice(0, 3).forEach(f => console.log(` - ${f}`));
console.log(` ... and ${matchedFiles.length - 3} more files`);
}
}
}
// Third pass: resolve transitive dependencies
console.log('');
console.log('=== Dependency Resolution ===');
const resolveDependent = (componentName, depth = 0) => {
for (const [dependent, dependencies] of dependencyGraph.entries()) {
if (dependencies.includes(componentName) && !affectedComponents.has(dependent)) {
affectedComponents.add(dependent);
// Track why this component was added
if (!componentTriggers.has(dependent)) {
componentTriggers.set(dependent, {
reason: 'dependency',
files: [],
dependency: componentName
});
console.log(`${' '.repeat(depth)}→ ${dependent} (depends on ${componentName})`);
}
resolveDependent(dependent, depth + 1); // Recursively add dependents
}
}
};
// Apply dependency resolution
const initialAffected = [...affectedComponents];
for (const comp of initialAffected) {
resolveDependent(comp, 1);
}
// Summary output
console.log('');
console.log('=== Summary ===');
console.log(`Initially affected: ${initialAffected.length} components`);
console.log(`After dependencies: ${affectedComponents.size} components`);
if (files.length <= 10) {
console.log('');
console.log('Changed files:');
files.forEach(f => console.log(` - ${f}`));
} else {
console.log(`Total files changed: ${files.length}`);
}
const groups = { rust:[], python:[], node:[], go:[], java:[], csharp:[], bdd:[], examples:[], other:[] };
// Process affected components and generate tasks
console.log('');
console.log('=== Task Generation ===');
const skippedComponents = [];
const taskedComponents = [];
for (const name of affectedComponents) {
const cfg = components[name];
const trigger = componentTriggers.get(name);
if (!cfg || !cfg.tasks || cfg.tasks.length === 0) {
skippedComponents.push(name);
continue;
}
const entries = cfg.tasks.map(task => ({ component: name, task }));
taskedComponents.push({ name, tasks: cfg.tasks, trigger });
if (name === 'rust') groups.rust.push(...entries);
else if (name === 'sdk-python') groups.python.push(...entries);
else if (name === 'sdk-node') groups.node.push(...entries);
else if (name === 'sdk-go') groups.go.push(...entries);
else if (name === 'sdk-java') groups.java.push(...entries);
else if (name === 'sdk-csharp') groups.csharp.push(...entries);
else if (name.startsWith('bdd-')) {
// Individual BDD tests should run separately with proper Docker setup
groups.bdd.push(...entries);
}
else if (name === 'examples-suite') {
// Examples should run separately
groups.examples.push(...entries);
}
else if (name === 'shell-scripts') groups.other.push(...entries);
else if (name === 'ci-workflows') groups.other.push(...entries);
else groups.other.push(...entries);
}
// Log components with tasks
if (taskedComponents.length > 0) {
console.log('Components with tasks to run:');
taskedComponents.forEach(({ name, tasks, trigger }) => {
const reason = trigger.reason === 'direct'
? `directly triggered by ${trigger.files.length} file(s)`
: `dependency of ${trigger.dependency}`;
console.log(` ✓ ${name}: ${tasks.length} task(s) - ${reason}`);
});
}
// Log skipped components (no tasks defined)
if (skippedComponents.length > 0) {
console.log('');
console.log('Components triggered but skipped (no tasks):');
skippedComponents.forEach(name => {
const trigger = componentTriggers.get(name);
const reason = trigger.reason === 'direct'
? `directly triggered by ${trigger.files.length} file(s)`
: `dependency of ${trigger.dependency}`;
console.log(` ○ ${name} - ${reason}`);
});
}
// On master push, run everything
if (context.eventName === 'push' && context.ref === 'refs/heads/master') {
// Clear existing groups to avoid duplicates - we'll run everything anyway
groups.rust = [];
groups.python = [];
groups.node = [];
groups.go = [];
groups.java = [];
groups.csharp = [];
groups.bdd = [];
groups.examples = [];
groups.other = [];
for (const [name, cfg] of Object.entries(components)) {
if (!cfg.tasks || cfg.tasks.length === 0) continue;
const entries = cfg.tasks.map(task => ({ component: name, task }));
if (name === 'rust') groups.rust.push(...entries);
else if (name === 'sdk-python') groups.python.push(...entries);
else if (name === 'sdk-node') groups.node.push(...entries);
else if (name === 'sdk-go') groups.go.push(...entries);
else if (name === 'sdk-java') groups.java.push(...entries);
else if (name === 'sdk-csharp') groups.csharp.push(...entries);
else if (name.startsWith('bdd-')) groups.bdd.push(...entries);
else if (name === 'examples-suite') groups.examples.push(...entries);
else groups.other.push(...entries);
}
}
// Deduplicate entries in each group (in case of any edge cases)
const dedupeGroup = (group) => {
const seen = new Set();
return group.filter(item => {
const key = `${item.component}-${item.task}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
};
// Apply deduplication to all groups
Object.keys(groups).forEach(key => {
groups[key] = dedupeGroup(groups[key]);
});
const matrix = a => a.length ? { include: a } : { include: [{ component: 'noop', task: 'noop' }] };
// Final summary of what will run
console.log('');
console.log('=== Test Jobs to Run ===');
const jobSummary = [
{ name: 'Rust', tasks: groups.rust },
{ name: 'Python SDK', tasks: groups.python },
{ name: 'Node SDK', tasks: groups.node },
{ name: 'Go SDK', tasks: groups.go },
{ name: 'Java SDK', tasks: groups.java },
{ name: 'C# SDK', tasks: groups.csharp },
{ name: 'BDD Tests', tasks: groups.bdd },
{ name: 'Examples', tasks: groups.examples },
{ name: 'Other', tasks: groups.other }
];
jobSummary.forEach(({ name, tasks }) => {
if (tasks.length > 0) {
const uniqueTasks = [...new Set(tasks.map(t => t.task))];
console.log(` ✓ ${name}: ${uniqueTasks.join(', ')}`);
} else {
console.log(` ✗ ${name}: SKIPPED (no changes detected)`);
}
});
// Overall stats
const totalTasks = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
console.log('');
console.log(`Total: ${affectedComponents.size} components affected, ${totalTasks} tasks to run`);
// Use environment files instead of deprecated setOutput
const setOutput = (name, value) => {
const output = `${name}=${value}`;
require('fs').appendFileSync(process.env.GITHUB_OUTPUT, `${output}\n`);
};
setOutput('rust_matrix', JSON.stringify(matrix(groups.rust)));
setOutput('python_matrix', JSON.stringify(matrix(groups.python)));
setOutput('node_matrix', JSON.stringify(matrix(groups.node)));
setOutput('go_matrix', JSON.stringify(matrix(groups.go)));
setOutput('java_matrix', JSON.stringify(matrix(groups.java)));
setOutput('csharp_matrix', JSON.stringify(matrix(groups.csharp)));
setOutput('bdd_matrix', JSON.stringify(matrix(groups.bdd)));
setOutput('examples_matrix', JSON.stringify(matrix(groups.examples)));
setOutput('other_matrix', JSON.stringify(matrix(groups.other)));