mirror of
https://github.com/immich-app/immich.git
synced 2025-08-11 09:16:31 -04:00
feat: shared pre-job action
This commit is contained in:
parent
53acf08263
commit
aee352e793
122
.github/actions/pre-job/action.yml
vendored
Normal file
122
.github/actions/pre-job/action.yml
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
name: 'Pre-Job'
|
||||
description: 'Determines which jobs should run based on changed paths'
|
||||
inputs:
|
||||
filters:
|
||||
description: 'Path filters as YAML string'
|
||||
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 (for special cases like weblate)'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
outputs:
|
||||
# Individual outputs that can be accessed directly
|
||||
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: Convert filters to JSON
|
||||
id: convert-filters
|
||||
shell: python
|
||||
run: |
|
||||
import json
|
||||
import os
|
||||
import yaml
|
||||
|
||||
# Get the filters input
|
||||
filters_yaml = """${{ inputs.filters }}"""
|
||||
|
||||
try:
|
||||
# Parse YAML properly using the yaml library
|
||||
filters_dict = yaml.safe_load(filters_yaml)
|
||||
|
||||
if not isinstance(filters_dict, dict):
|
||||
raise ValueError("Filters must be a YAML dictionary")
|
||||
|
||||
if not filters_dict:
|
||||
raise ValueError("No valid filters found")
|
||||
|
||||
# We only need the filter names (keys), not the actual path arrays
|
||||
filter_names = {name: [] for name in filters_dict.keys()}
|
||||
|
||||
filters_json = json.dumps(filter_names)
|
||||
|
||||
print("Converted filters to JSON:")
|
||||
print(filters_json)
|
||||
|
||||
# Set GitHub Actions output
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write(f"filters_json={filters_json}\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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
force-events: ${{ inputs.force-events }}
|
||||
force-branches: ${{ inputs.force-branches }}
|
||||
exclude-branches: ${{ inputs.exclude-branches }}
|
||||
skip-force-logic: ${{ inputs.skip-force-logic }}
|
||||
filters-json: ${{ steps.convert-filters.outputs.filters_json }}
|
||||
script: |
|
||||
const script = require('./.github/actions/pre-job/check-conditions.js')
|
||||
script({ core, context })
|
||||
|
||||
- name: Checkout code
|
||||
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' }}
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check force paths
|
||||
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' && inputs.force-filters != '' }}
|
||||
id: force_paths
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
force-paths:
|
||||
${{ inputs.force-filters }}
|
||||
|
||||
- name: Check main paths
|
||||
if: ${{ steps.check-conditions.outputs.needs_path_filtering == 'true' && (inputs.force-filters == '' || steps.force_paths.outputs.force-paths != 'true') }}
|
||||
id: main_paths
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: ${{ inputs.filters }}
|
||||
|
||||
- name: Generate final outputs
|
||||
id: generate-outputs
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
filters-json: ${{ steps.convert-filters.outputs.filters_json }}
|
||||
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.outputs) }}
|
||||
main-path-results: ${{ toJSON(steps.main_paths.outputs) }}
|
||||
script: |
|
||||
const script = require('./.github/actions/pre-job/generate-outputs.js')
|
||||
script({ core })
|
105
.github/actions/pre-job/check-conditions.js
vendored
Normal file
105
.github/actions/pre-job/check-conditions.js
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
module.exports = ({ core, context }) => {
|
||||
console.log('=== Pre-Job: Checking Conditions ===');
|
||||
|
||||
// Get inputs directly from core
|
||||
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.getInput('skip-force-logic') === 'true';
|
||||
const filtersJson = core.getInput('filters-json');
|
||||
|
||||
// Parse JSON filters (much more reliable than YAML parsing)
|
||||
let filterNames = [];
|
||||
try {
|
||||
const filters = JSON.parse(filtersJson);
|
||||
filterNames = Object.keys(filters);
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to parse filters JSON: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get GitHub context
|
||||
const currentEvent = context.eventName;
|
||||
// Fix: Handle different ref types safely
|
||||
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,
|
||||
});
|
||||
|
||||
// Validate inputs
|
||||
if (!filtersJson || !filtersJson.trim()) {
|
||||
core.setFailed('filters-json input is required and cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterNames.length === 0) {
|
||||
core.setFailed('No valid filters found in filters-json input');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
if (forceTriggered) {
|
||||
const reason = eventForce
|
||||
? `event: ${currentEvent}`
|
||||
: `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);
|
||||
};
|
97
.github/actions/pre-job/generate-outputs.js
vendored
Normal file
97
.github/actions/pre-job/generate-outputs.js
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
// No longer need YAML parser - using JSON from Python conversion
|
||||
module.exports = ({ core }) => {
|
||||
console.log('=== Pre-Job: Generating Final Outputs ===');
|
||||
|
||||
try {
|
||||
// Get inputs directly from core
|
||||
const filtersJson = core.getInput('filters-json');
|
||||
const skipForceLogic = core.getInput('skip-force-logic') === 'true';
|
||||
|
||||
// Get step outputs
|
||||
const forceTriggered = core.getInput('force-triggered') === 'true';
|
||||
const shouldSkip = core.getInput('should-skip') === 'true';
|
||||
const needsPathFiltering = core.getInput('needs-path-filtering') === 'true';
|
||||
|
||||
// Parse path results from separate steps
|
||||
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 JSON filters (much more reliable than YAML parsing)
|
||||
let filterNames = [];
|
||||
try {
|
||||
const filters = JSON.parse(filtersJson);
|
||||
filterNames = Object.keys(filters);
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to parse filters JSON: ${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');
|
||||
|
||||
// Check if force paths triggered (this forces ALL filters to true)
|
||||
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');
|
||||
// Process each filter based on main path results
|
||||
for (const filterName of filterNames) {
|
||||
const pathResult = mainPathResults[filterName] === 'true';
|
||||
results[filterName] = pathResult;
|
||||
|
||||
console.log(`Filter ${filterName}: ${pathResult}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output as JSON object that can be accessed with fromJSON()
|
||||
core.setOutput('should_run', JSON.stringify(results));
|
||||
|
||||
console.log('✅ Final results:', results);
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to generate outputs: ${error.message}`);
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user