mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2025-12-29 00:24:57 +01:00
fix(validator): add support for GitHub Actions reusable workflow validation
Enhance the workflow validator to correctly handle GitHub Actions reusable workflows: - Skip validation of 'runs-on' field for jobs with a 'uses' field - Skip validation of the 'steps' field for reusable workflow jobs - Add specific format validation for reusable workflow references - Make workflow name optional for files that only contain reusable workflow jobs - Add comprehensive test workflow files for various reusable workflow scenarios This fixes validation errors when using GitHub's workflow_call feature that allows reusing workflows from other repositories or local files.
This commit is contained in:
@@ -23,7 +23,23 @@ pub fn evaluate_workflow_file(path: &Path, verbose: bool) -> Result<ValidationRe
|
||||
|
||||
// Check if name exists
|
||||
if workflow.get("name").is_none() {
|
||||
result.add_issue("Workflow is missing a name".to_string());
|
||||
// Check if this might be a reusable workflow caller before reporting missing name
|
||||
let has_reusable_workflow_job = if let Some(Value::Mapping(jobs)) = workflow.get("jobs") {
|
||||
jobs.values().any(|job| {
|
||||
if let Some(job_config) = job.as_mapping() {
|
||||
job_config.contains_key(Value::String("uses".to_string()))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Only report missing name if it's not a workflow with reusable workflow jobs
|
||||
if !has_reusable_workflow_job {
|
||||
result.add_issue("Workflow is missing a name".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if jobs section exists
|
||||
|
||||
@@ -8,6 +8,7 @@ mod matrix;
|
||||
mod matrix_test;
|
||||
mod models;
|
||||
mod parser;
|
||||
mod reusable_workflow_test;
|
||||
mod runtime;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
67
src/reusable_workflow_test.rs
Normal file
67
src/reusable_workflow_test.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::evaluator::evaluate_workflow_file;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_reusable_workflow_validation() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let workflow_path = temp_dir.path().join("test-workflow.yml");
|
||||
|
||||
// Create a workflow file that uses reusable workflows
|
||||
let content = r#"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-workflow-1-in-local-repo:
|
||||
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
|
||||
call-workflow-2-in-local-repo:
|
||||
uses: ./path/to/workflow.yml
|
||||
with:
|
||||
username: mona
|
||||
secrets:
|
||||
token: ${{ secrets.TOKEN }}
|
||||
"#;
|
||||
|
||||
fs::write(&workflow_path, content).unwrap();
|
||||
|
||||
// Validate the workflow
|
||||
let result = evaluate_workflow_file(&workflow_path, false).unwrap();
|
||||
|
||||
// Should be valid since we've fixed the validation to handle reusable workflows
|
||||
assert!(
|
||||
result.is_valid,
|
||||
"Workflow should be valid, but got issues: {:?}",
|
||||
result.issues
|
||||
);
|
||||
assert!(result.issues.is_empty());
|
||||
|
||||
// Create an invalid reusable workflow (bad format for 'uses')
|
||||
let invalid_content = r#"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-workflow-invalid:
|
||||
uses: invalid-format
|
||||
"#;
|
||||
|
||||
fs::write(&workflow_path, invalid_content).unwrap();
|
||||
|
||||
// Validate the workflow
|
||||
let result = evaluate_workflow_file(&workflow_path, false).unwrap();
|
||||
|
||||
// Should be invalid due to the bad format
|
||||
assert!(!result.is_valid);
|
||||
assert!(result
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.contains("Invalid reusable workflow reference format")));
|
||||
}
|
||||
}
|
||||
@@ -12,34 +12,55 @@ pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {
|
||||
for (job_name, job_config) in jobs_map {
|
||||
if let Some(job_name) = job_name.as_str() {
|
||||
if let Some(job_config) = job_config.as_mapping() {
|
||||
// Check for required 'runs-on'
|
||||
if !job_config.contains_key(Value::String("runs-on".to_string())) {
|
||||
// Check if this is a reusable workflow job (has 'uses' field)
|
||||
let is_reusable_workflow =
|
||||
job_config.contains_key(Value::String("uses".to_string()));
|
||||
|
||||
// Only check for 'runs-on' if it's not a reusable workflow
|
||||
if !is_reusable_workflow
|
||||
&& !job_config.contains_key(Value::String("runs-on".to_string()))
|
||||
{
|
||||
result.add_issue(format!("Job '{}' is missing 'runs-on' field", job_name));
|
||||
}
|
||||
|
||||
// Check for steps
|
||||
match job_config.get(Value::String("steps".to_string())) {
|
||||
Some(Value::Sequence(steps)) => {
|
||||
if steps.is_empty() {
|
||||
// Only check for steps if it's not a reusable workflow
|
||||
if !is_reusable_workflow {
|
||||
match job_config.get(Value::String("steps".to_string())) {
|
||||
Some(Value::Sequence(steps)) => {
|
||||
if steps.is_empty() {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' has empty 'steps' section",
|
||||
job_name
|
||||
));
|
||||
} else {
|
||||
validate_steps(steps, job_name, result);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' has empty 'steps' section",
|
||||
"Job '{}': 'steps' section is not a sequence",
|
||||
job_name
|
||||
));
|
||||
}
|
||||
None => {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' is missing 'steps' section",
|
||||
job_name
|
||||
));
|
||||
} else {
|
||||
validate_steps(steps, job_name, result);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
result.add_issue(format!(
|
||||
"Job '{}': 'steps' section is not a sequence",
|
||||
job_name
|
||||
));
|
||||
}
|
||||
None => {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' is missing 'steps' section",
|
||||
job_name
|
||||
));
|
||||
} else {
|
||||
// For reusable workflows, validate the 'uses' field format
|
||||
if let Some(Value::String(uses)) =
|
||||
job_config.get(Value::String("uses".to_string()))
|
||||
{
|
||||
// Simple validation for reusable workflow reference format
|
||||
if !uses.contains('/') || !uses.contains('.') {
|
||||
result.add_issue(format!(
|
||||
"Job '{}': Invalid reusable workflow reference format '{}'",
|
||||
job_name, uses
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
test-workflows/1-basic-workflow.yml
Normal file
21
test-workflows/1-basic-workflow.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Basic Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
20
test-workflows/2-reusable-workflow-caller.yml
Normal file
20
test-workflows/2-reusable-workflow-caller.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Reusable Workflow Caller
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
call-workflow-1:
|
||||
uses: octo-org/example-repo/.github/workflows/workflow-A.yml@v1
|
||||
|
||||
call-workflow-2:
|
||||
uses: ./local-workflows/build.yml
|
||||
with:
|
||||
config-path: ./config/test.yml
|
||||
secrets:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
call-workflow-3:
|
||||
uses: octo-org/example-repo/.github/workflows/workflow-B.yml@main
|
||||
needs: [call-workflow-1]
|
||||
32
test-workflows/3-reusable-workflow-definition.yml
Normal file
32
test-workflows/3-reusable-workflow-definition.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Reusable Workflow Definition
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
config-path:
|
||||
required: true
|
||||
type: string
|
||||
description: "Path to the configuration file"
|
||||
environment:
|
||||
required: false
|
||||
type: string
|
||||
default: "production"
|
||||
description: "Environment to run in"
|
||||
secrets:
|
||||
token:
|
||||
required: true
|
||||
description: "GitHub token for authentication"
|
||||
|
||||
jobs:
|
||||
reusable-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Load configuration
|
||||
run: echo "Loading configuration from ${{ inputs.config-path }}"
|
||||
- name: Run in environment
|
||||
run: echo "Running in ${{ inputs.environment }} environment"
|
||||
- name: Use secret
|
||||
run: echo "Using secret with length ${#TOKEN}"
|
||||
env:
|
||||
TOKEN: ${{ secrets.token }}
|
||||
25
test-workflows/4-mixed-jobs.yml
Normal file
25
test-workflows/4-mixed-jobs.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Mixed Regular and Reusable Jobs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
regular-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run regular task
|
||||
run: echo "This is a regular job"
|
||||
|
||||
reusable-job:
|
||||
uses: octo-org/example-repo/.github/workflows/reusable.yml@main
|
||||
with:
|
||||
parameter: "value"
|
||||
|
||||
dependent-job:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [regular-job, reusable-job]
|
||||
steps:
|
||||
- name: Run dependent task
|
||||
run: echo "This job depends on both a regular and reusable job"
|
||||
12
test-workflows/5-no-name-reusable-caller.yml
Normal file
12
test-workflows/5-no-name-reusable-caller.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
call-workflow-1:
|
||||
uses: octo-org/example-repo/.github/workflows/workflow-A.yml@v1
|
||||
|
||||
call-workflow-2:
|
||||
uses: ./local-workflows/build.yml
|
||||
with:
|
||||
config-path: ./config/test.yml
|
||||
17
test-workflows/6-invalid-reusable-format.yml
Normal file
17
test-workflows/6-invalid-reusable-format.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Invalid Reusable Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
valid-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Test step
|
||||
run: echo "This is a valid job"
|
||||
|
||||
invalid-reusable-job:
|
||||
uses: invalid-format
|
||||
with:
|
||||
param: "value"
|
||||
19
test-workflows/7-invalid-regular-job.yml
Normal file
19
test-workflows/7-invalid-regular-job.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Invalid Regular Job
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
job-missing-runs-on:
|
||||
# Missing runs-on field
|
||||
steps:
|
||||
- name: Test step
|
||||
run: echo "This job is missing runs-on field"
|
||||
|
||||
job-missing-steps:
|
||||
runs-on: ubuntu-latest
|
||||
# Missing steps section
|
||||
|
||||
valid-reusable-job:
|
||||
uses: octo-org/example-repo/.github/workflows/reusable.yml@main
|
||||
31
test-workflows/8-cyclic-dependencies.yml
Normal file
31
test-workflows/8-cyclic-dependencies.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Cyclic Dependencies
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
job-a:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job-c]
|
||||
steps:
|
||||
- name: Job A
|
||||
run: echo "Job A"
|
||||
|
||||
job-b:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job-a]
|
||||
steps:
|
||||
- name: Job B
|
||||
run: echo "Job B"
|
||||
|
||||
job-c:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job-b]
|
||||
steps:
|
||||
- name: Job C
|
||||
run: echo "Job C"
|
||||
|
||||
reusable-job:
|
||||
uses: octo-org/example-repo/.github/workflows/reusable.yml@main
|
||||
needs: [job-a]
|
||||
Reference in New Issue
Block a user