diff --git a/README.md b/README.md index 3ab908f..fa7c56c 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ jobs: uses: re-actors/alls-green@release/v1 with: allowed-failures: docs, linters + allowed-skips: non-voting-flaky-job jobs: ${{ toJSON(needs) }} ... ``` @@ -85,10 +86,13 @@ jobs: ## Options -There are two options — `allowed-failures` and `jobs`. The former is -optional but the later is mandatory. `allowed-failures` tells the action -which jobs should not affect the outcome, by default all the jobs will -be "voting". `jobs` is an object representing the jobs that should +There are three options — `allowed-failures`, `allowed-skips` and +`jobs`. The first two are optional but `jobs` is mandatory. +`allowed-failures` tells the action which jobs should not affect the +outcome if they don't succeed, by default all the jobs will +be "voting". Same goes for `allowed-skips` — it won't allow the listed +jobs to affect the outcome if they are skipped but are still "voting" in +case they run. `jobs` is an object representing the jobs that should affect the decision of whether the pipeline failed or not, it is important to pass a JSON-serialized `needs` context to this argument. diff --git a/action.yml b/action.yml index 59b69d6..fc67aa9 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,13 @@ inputs: Job names that are allowed to fail and not affect the outcome, as a comma-separated list or serialized as a JSON string required: false + allowed-skips: + default: >- + [] + description: >- + Job names that are allowed to be skipped and not affect the + outcome, as a comma-separated list or serialized as a JSON string + required: false outputs: failure: @@ -48,6 +55,10 @@ runs: ${{ inputs.allowed-failures }} EOM )" \ + "$(cat << EOM + ${{ inputs.allowed-skips }} + EOM + )" \ "$(cat << EOM ${{ inputs.jobs }} EOM diff --git a/src/normalize_needed_jobs_status.py b/src/normalize_needed_jobs_status.py index 07df075..77fd079 100755 --- a/src/normalize_needed_jobs_status.py +++ b/src/normalize_needed_jobs_status.py @@ -35,13 +35,14 @@ def parse_as_list(input_text): return [s.strip() for s in input_text.split(',')] -def parse_inputs(raw_allowed_failures, raw_jobs): +def parse_inputs(raw_allowed_failures, raw_allowed_skips, raw_jobs): """Normalize the action inputs by turning them into data.""" allowed_failures_input = parse_as_list(raw_allowed_failures) - + allowed_skips_input = parse_as_list(raw_allowed_skips) return { 'allowed_failures': allowed_failures_input, + 'allowed_skips': allowed_skips_input, 'jobs': json.loads(raw_jobs), } @@ -49,7 +50,9 @@ def parse_inputs(raw_allowed_failures, raw_jobs): def log_decision_details( job_matrix_succeeded, jobs_allowed_to_fail, + jobs_allowed_to_be_skipped, allowed_to_fail_jobs_succeeded, + allowed_to_be_skipped_jobs_succeeded, jobs, ): """Record the decisions made into console output.""" @@ -73,27 +76,46 @@ def log_decision_details( ) + if jobs_allowed_to_be_skipped and allowed_to_be_skipped_jobs_succeeded: + print_to_stderr( + '🛈 All of the allowed to be skipped dependency jobs succeeded.', + ) + elif jobs_allowed_to_fail: + print_to_stderr( + '🛈 Some of the allowed to be skipped jobs did not succeed.', + ) + + print_to_stderr('📝 Job statuses:') for name, job in jobs.items(): print_to_stderr( '📝 {name} → {emoji} {result} [{status}]'. format( - emoji='✓' if job['result'] == 'success' else '❌', + emoji='✓' if job['result'] == 'success' + else '❌' if job['result'] == 'failure' + else '⬜', name=name, result=job['result'], status='allowed to fail' if name in jobs_allowed_to_fail - else 'required to succeed', + else 'required to succeed' + if name not in jobs_allowed_to_be_skipped + else 'required to succeed or be skipped', ), ) def main(argv): """Decide whether the needed jobs got satisfactory results.""" - inputs = parse_inputs(raw_allowed_failures=argv[1], raw_jobs=argv[2]) + inputs = parse_inputs( + raw_allowed_failures=argv[1], + raw_allowed_skips=argv[2], + raw_jobs=argv[3], + ) jobs = inputs['jobs'] or {} - jobs_allowed_to_fail = inputs['allowed_failures'] or [] + jobs_allowed_to_fail = set(inputs['allowed_failures'] or []) + jobs_allowed_to_be_skipped = set(inputs['allowed_skips'] or []) if not jobs: sys.exit( @@ -104,7 +126,10 @@ def main(argv): job_matrix_succeeded = all( job['result'] == 'success' for name, job in jobs.items() - if name not in jobs_allowed_to_fail + if name not in (jobs_allowed_to_fail | jobs_allowed_to_be_skipped) + ) and all( + job['result'] in {'skipped', 'success'} for name, job in jobs.items() + if name in jobs_allowed_to_be_skipped ) set_final_result_outputs(job_matrix_succeeded) @@ -115,10 +140,18 @@ def main(argv): ) + allowed_to_be_skipped_jobs_succeeded = all( + job['result'] == 'success' for name, job in jobs.items() + if name in jobs_allowed_to_be_skipped + ) + + log_decision_details( job_matrix_succeeded, jobs_allowed_to_fail, + jobs_allowed_to_be_skipped, allowed_to_fail_jobs_succeeded, + allowed_to_be_skipped_jobs_succeeded, jobs, )