mirror of
https://github.com/immich-app/devtools.git
synced 2026-09-30 21:27:48 +08:00
373 lines
14 KiB
YAML
373 lines
14 KiB
YAML
name: 'Pre-Job'
|
|
description: 'Determines which jobs should run based on changed paths'
|
|
inputs:
|
|
filters:
|
|
description: 'Path filters as YAML string'
|
|
required: true
|
|
github-token:
|
|
description: 'Github token to use'
|
|
required: true
|
|
force-filters:
|
|
description: 'Additional path filters that trigger force-run (e.g., workflow files)'
|
|
required: false
|
|
default: ''
|
|
force-events:
|
|
description: 'Events that should force all jobs to run (comma-separated)'
|
|
required: false
|
|
default: 'workflow_dispatch'
|
|
force-branches:
|
|
description: 'Branches that should force all jobs to run (comma-separated)'
|
|
required: false
|
|
default: ''
|
|
exclude-branches:
|
|
description: 'Branches to exclude from running (comma-separated)'
|
|
required: false
|
|
default: ''
|
|
skip-force-logic:
|
|
description: 'Skip the standard force logic'
|
|
required: false
|
|
default: 'false'
|
|
force:
|
|
description: 'Custom boolean condition to force all jobs to run'
|
|
required: false
|
|
default: 'false'
|
|
persist-credentials:
|
|
description: 'Persist git credentials after checkout. Needed on private repos for path filtering on push; leaves the token in .git/config'
|
|
required: false
|
|
default: 'false'
|
|
|
|
outputs:
|
|
should_run:
|
|
description: 'Nested object with filter results (access via fromJSON(steps.pre-job.outputs.should_run).filter_name)'
|
|
value: ${{ steps.generate-outputs.outputs.should_run }}
|
|
|
|
runs:
|
|
using: 'composite'
|
|
steps:
|
|
- name: List filter keys as comma-separated string
|
|
id: list-filters
|
|
shell: python
|
|
env:
|
|
FILTERS: ${{ inputs.filters }}
|
|
run: |
|
|
import json
|
|
import os
|
|
import yaml
|
|
|
|
filters_yaml = os.environ['FILTERS']
|
|
|
|
try:
|
|
filters_dict = yaml.safe_load(filters_yaml)
|
|
|
|
if not filters_dict:
|
|
raise ValueError("No valid filters found")
|
|
|
|
if not isinstance(filters_dict, dict):
|
|
raise ValueError("Filters must be a YAML dictionary")
|
|
|
|
filter_names = ','.join(filters_dict.keys())
|
|
|
|
print(f"Filter names: {filter_names}")
|
|
|
|
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
|
f.write(f"filters={filter_names}\n")
|
|
|
|
except Exception as e:
|
|
print(f"Error converting filters: {e}")
|
|
exit(1)
|
|
|
|
- name: Check conditions and determine if path filtering is needed
|
|
id: check-conditions
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
force-events: ${{ inputs.force-events }}
|
|
force-branches: ${{ inputs.force-branches }}
|
|
exclude-branches: ${{ inputs.exclude-branches }}
|
|
skip-force-logic: ${{ inputs.skip-force-logic }}
|
|
force: ${{ inputs.force }}
|
|
filters-list: ${{ steps.list-filters.outputs.filters }}
|
|
script: |
|
|
console.log('=== Pre-Job: Checking Conditions ===');
|
|
|
|
const forceEvents = core
|
|
.getInput('force-events')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const forceBranches = core
|
|
.getInput('force-branches')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const excludeBranches = core
|
|
.getInput('exclude-branches')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const skipForceLogic = core.getBooleanInput('skip-force-logic');
|
|
const forceInput = core.getBooleanInput('force');
|
|
const filtersList = core.getInput('filters-list');
|
|
|
|
let filterNames = [];
|
|
try {
|
|
if (!filtersList || !filtersList.trim()) {
|
|
throw new Error('filters-list input is required and cannot be empty');
|
|
}
|
|
|
|
filterNames = filtersList
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
if (filterNames.length === 0) {
|
|
throw new Error('No valid filter names found');
|
|
}
|
|
} catch (error) {
|
|
core.setFailed(`Failed to parse filter names: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
const currentEvent = context.eventName;
|
|
const currentBranch = context.ref?.startsWith('refs/heads/')
|
|
? context.ref.replace('refs/heads/', '')
|
|
: context.ref || '';
|
|
const currentHeadRef = context.payload.pull_request?.head?.ref || '';
|
|
|
|
console.log('Context:', {
|
|
event: currentEvent,
|
|
branch: currentBranch,
|
|
headRef: currentHeadRef,
|
|
filterCount: filterNames.length,
|
|
});
|
|
|
|
console.log('Configuration:', {
|
|
forceEvents,
|
|
forceBranches,
|
|
excludeBranches,
|
|
skipForceLogic,
|
|
});
|
|
|
|
// Step 1: Check exclusion conditions (fastest short-circuit)
|
|
const shouldSkip = excludeBranches.some(
|
|
(branch) => currentHeadRef === branch,
|
|
);
|
|
|
|
if (shouldSkip) {
|
|
console.log(`🚫 EXCLUDED: Branch ${currentHeadRef} is in exclude list`);
|
|
core.setOutput('should_skip', true);
|
|
core.setOutput('force_triggered', false);
|
|
core.setOutput('needs_path_filtering', false);
|
|
return;
|
|
}
|
|
|
|
// Step 2: Check force conditions (no checkout needed)
|
|
let forceTriggered = false;
|
|
if (!skipForceLogic) {
|
|
const eventForce = forceEvents.includes(currentEvent);
|
|
const branchForce = forceBranches.includes(currentBranch);
|
|
forceTriggered = eventForce || branchForce || forceInput;
|
|
|
|
if (forceTriggered) {
|
|
let reason;
|
|
if (forceInput) {
|
|
reason = 'custom force input';
|
|
} else if (eventForce) {
|
|
reason = `event: ${currentEvent}`;
|
|
} else {
|
|
reason = `branch: ${currentBranch}`;
|
|
}
|
|
console.log(`🚀 FORCED: Triggered by ${reason}`);
|
|
core.setOutput('should_skip', false);
|
|
core.setOutput('force_triggered', true);
|
|
core.setOutput('needs_path_filtering', false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Step 3: Need to do path filtering
|
|
console.log(
|
|
'📁 PATH FILTERING: No force conditions met, need to check changed paths',
|
|
);
|
|
core.setOutput('should_skip', false);
|
|
core.setOutput('force_triggered', false);
|
|
core.setOutput('needs_path_filtering', true);
|
|
|
|
- name: Checkout code
|
|
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' }}
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: ${{ inputs.persist-credentials }}
|
|
token: ${{ inputs.github-token }}
|
|
|
|
- name: Check force paths
|
|
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' && inputs.force-filters != '' }}
|
|
id: force_paths
|
|
shell: python
|
|
env:
|
|
FORCE_FILTERS: ${{ inputs.force-filters }}
|
|
GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }}
|
|
run: |
|
|
import os
|
|
import yaml
|
|
|
|
force_filters_input = os.environ.get('FORCE_FILTERS', '').strip()
|
|
|
|
if not force_filters_input:
|
|
print("No force filters provided")
|
|
exit(0)
|
|
|
|
try:
|
|
force_paths_list = yaml.safe_load(force_filters_input)
|
|
|
|
if not isinstance(force_paths_list, list):
|
|
raise ValueError("force-filters must be a YAML array of paths")
|
|
|
|
if not force_paths_list:
|
|
print("No valid paths found in force-filters")
|
|
exit(0)
|
|
|
|
force_paths_config = {
|
|
'force-paths': force_paths_list
|
|
}
|
|
|
|
force_paths_yaml = yaml.dump(force_paths_config, default_flow_style=False)
|
|
|
|
print(f"Generated force paths YAML: {force_paths_yaml}")
|
|
|
|
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
|
f.write(f"force-paths-yaml<<EOF\n{force_paths_yaml}EOF\n")
|
|
|
|
except Exception as e:
|
|
print(f"Error processing force filters: {e}")
|
|
print(f"Input was: {repr(force_filters_input)}")
|
|
exit(1)
|
|
|
|
- name: Run force paths filter
|
|
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' && inputs.force-filters != '' }}
|
|
id: force_paths_filter
|
|
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
|
with:
|
|
filters: ${{ steps.force_paths.outputs.force-paths-yaml }}
|
|
base: ${{ github.event_name == 'push' && github.ref_name || '' }}
|
|
token: ${{ inputs.github-token }}
|
|
|
|
- name: Check main paths
|
|
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' && (inputs.force-filters == '' || steps.force_paths_filter.outputs.force-paths != 'true') }}
|
|
id: main_paths
|
|
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
|
with:
|
|
filters: ${{ inputs.filters }}
|
|
base: ${{ github.event_name == 'push' && github.ref_name || '' }}
|
|
token: ${{ inputs.github-token }}
|
|
|
|
- name: Generate final outputs
|
|
id: generate-outputs
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
filters-list: ${{ steps.list-filters.outputs.filters }}
|
|
skip-force-logic: ${{ inputs.skip-force-logic }}
|
|
force-triggered: ${{ steps.check-conditions.outputs.force_triggered }}
|
|
should-skip: ${{ steps.check-conditions.outputs.should_skip }}
|
|
needs-path-filtering: ${{ steps.check-conditions.outputs.needs_path_filtering }}
|
|
force-path-results: ${{ toJSON(steps.force_paths_filter.outputs) }}
|
|
main-path-results: ${{ toJSON(steps.main_paths.outputs) }}
|
|
script: |
|
|
console.log('=== Pre-Job: Generating Final Outputs ===');
|
|
|
|
try {
|
|
const filtersList = core.getInput('filters-list');
|
|
const skipForceLogic = core.getBooleanInput('skip-force-logic');
|
|
|
|
const forceTriggered = core.getBooleanInput('force-triggered');
|
|
const shouldSkip = core.getBooleanInput('should-skip');
|
|
const needsPathFiltering = core.getBooleanInput('needs-path-filtering');
|
|
|
|
let forcePathResults = {};
|
|
let mainPathResults = {};
|
|
|
|
try {
|
|
const forcePathRaw = core.getInput('force-path-results');
|
|
if (forcePathRaw && forcePathRaw !== '{}') {
|
|
forcePathResults = JSON.parse(forcePathRaw);
|
|
}
|
|
} catch (e) {
|
|
console.log('No force path results or parse error:', e.message);
|
|
}
|
|
|
|
try {
|
|
const mainPathRaw = core.getInput('main-path-results');
|
|
if (mainPathRaw && mainPathRaw !== '{}') {
|
|
mainPathResults = JSON.parse(mainPathRaw);
|
|
}
|
|
} catch (e) {
|
|
console.log('No main path results or parse error:', e.message);
|
|
}
|
|
|
|
// Parse filter names from comma-separated string
|
|
let filterNames = [];
|
|
try {
|
|
if (!filtersList || !filtersList.trim()) {
|
|
throw new Error('filters-list input is required and cannot be empty');
|
|
}
|
|
|
|
filterNames = filtersList
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
if (filterNames.length === 0) {
|
|
throw new Error('No valid filter names found');
|
|
}
|
|
} catch (error) {
|
|
core.setFailed(`Failed to parse filter names: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
console.log('Processing filters:', filterNames);
|
|
|
|
const results = {};
|
|
|
|
// Handle early exit scenarios
|
|
if (shouldSkip) {
|
|
console.log('🚫 Generating SKIP results (all false)');
|
|
for (const filterName of filterNames) {
|
|
results[filterName] = false;
|
|
}
|
|
} else if (forceTriggered && !skipForceLogic) {
|
|
console.log('🚀 Generating FORCE results (all true)');
|
|
for (const filterName of filterNames) {
|
|
results[filterName] = true;
|
|
}
|
|
} else if (!needsPathFiltering) {
|
|
console.log('⚡ No path filtering needed, all false');
|
|
for (const filterName of filterNames) {
|
|
results[filterName] = false;
|
|
}
|
|
} else {
|
|
console.log('📁 Generating PATH-BASED results');
|
|
|
|
const forcePathsTriggered = forcePathResults['force-paths'] === 'true';
|
|
|
|
if (forcePathsTriggered && !skipForceLogic) {
|
|
console.log('🚀 FORCE-PATHS triggered - all filters true');
|
|
for (const filterName of filterNames) {
|
|
results[filterName] = true;
|
|
}
|
|
} else {
|
|
console.log('📋 Using individual path results');
|
|
for (const filterName of filterNames) {
|
|
const pathResult = mainPathResults[filterName] === 'true';
|
|
results[filterName] = pathResult;
|
|
|
|
console.log(`Filter ${filterName}: ${pathResult}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
core.setOutput('should_run', JSON.stringify(results));
|
|
|
|
console.log('✅ Final results:', results);
|
|
} catch (error) {
|
|
core.setFailed(`Failed to generate outputs: ${error.message}`);
|
|
}
|