diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 53d126b..286f353 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -103,6 +103,8 @@ jobs: token: '${{ secrets.CI_PUSH_TO_PROTECTED_BRANCH }}' branch: protected unprotect_reviews: true + acceptable_conclusions: success,skipped + debug: true - name: Pushing to a protected branch without any changes uses: ./ @@ -110,6 +112,8 @@ jobs: token: '${{ secrets.CI_PUSH_TO_PROTECTED_BRANCH }}' ref: refs/heads/protected unprotect_reviews: true + acceptable_conclusions: success,skipped + debug: true force-pushing: needs: [protected] @@ -138,6 +142,7 @@ jobs: token: '${{ secrets.CI_PUSH_TO_PROTECTED_BRANCH }}' branch: protected unprotect_reviews: true + acceptable_conclusions: success,skipped - name: This runs ONLY if the previous step doesn't fail if: steps.push_no_force.outcome != 'failure' || steps.push_no_force.conclusion != 'success' @@ -152,6 +157,7 @@ jobs: branch: protected unprotect_reviews: true force: yes + acceptable_conclusions: success,skipped branch_and_ref: needs: [force-pushing] @@ -220,6 +226,31 @@ jobs: force: yes debug: true + acceptable_conclusions: + needs: [path] + runs-on: ubuntu-latest + name: Testing - Default for `acceptable_conclusions` with skipped checks + steps: + - name: Use local action (checkout) + uses: actions/checkout@v4 + + - name: Push to protected branch without 'skipped' in 'acceptable_conclusions' + id: push_skipped + continue-on-error: true + uses: ./ + with: + token: '${{ secrets.CI_PUSH_TO_PROTECTED_BRANCH }}' + branch: protected + unprotect_reviews: true + # Default value - set here explicitly for clarity + acceptable_conclusions: success + + - name: This runs ONLY if the previous step doesn't fail + if: steps.push_skipped.outcome != 'failure' || steps.push_skipped.conclusion != 'success' + run: | + echo "Outcome: ${{ steps.push_skipped.outcome }} (not 'failure'), Conclusion: ${{ steps.push_skipped.conclusion }} (not 'success')" + exit 1 + pre-commit: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/test_status_checks.yml b/.github/workflows/test_status_checks.yml index 52815b0..f306ad9 100644 --- a/.github/workflows/test_status_checks.yml +++ b/.github/workflows/test_status_checks.yml @@ -22,3 +22,11 @@ jobs: steps: - name: Important status check run: echo "Very important status check - SUCCESS!" + + skipped_mock_status_check: + runs-on: ubuntu-latest + name: Skipped Mock Status Check + if: ! always() + steps: + - name: Skipped status check + run: echo "Very important skipped status check - SKIPPED!" diff --git a/README.md b/README.md index 3a4de3b..cf7c0db 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ All input names in **bold** are _required_. | `unprotect_reviews` | Momentarily remove pull request review protection from target branch.
**Note**: One needs administrative access to the repository to be able to use this feature. This means two things need to match up: The PAT must represent a user with administrative rights, and these rights need to be granted to the usage scope of the PAT. | `False` | | `debug` | Set `set -x` in `entrypoint.sh` when running the action. This is for debugging the action. | `False` | | `path` | A path to the working directory of the action. This should be relative to the `$GITHUB_WORKSPACE`. | `.` | +| `acceptable_conclusions` | A comma-separated list of acceptable statuses. If any of these statuses are present, the action will not fail.

See the [GitHub REST API documentation](https://docs.github.com/en/rest/actions/workflow-jobs#get-a-job-for-a-workflow-run), specifically, the Response schema's "conclusion" property's `enum` values, for a complete list of supported values (excluding `null`). | `success` | ## License diff --git a/action.yml b/action.yml index 695904c..3ca3f87 100644 --- a/action.yml +++ b/action.yml @@ -48,6 +48,10 @@ inputs: description: 'A path to the working directory of the action. This should be relative to the $GITHUB_WORKSPACE.' required: false default: '.' + acceptable_conclusions: + description: 'A comma-separated list of acceptable conclusions. If any of these conclusions are present, the action will not fail.' + required: false + default: 'success' runs: using: 'docker' image: 'Dockerfile' diff --git a/entrypoint.sh b/entrypoint.sh index b1d07d6..2f067bb 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -17,7 +17,13 @@ unprotect () { y | Y | yes | Yes | YES | true | True | TRUE | on | On | ON) if [ -n "${PUSH_PROTECTED_CHANGED_BRANCH}" ] && [ -n "${PUSH_PROTECTED_PROTECTED_BRANCH}" ]; then echo -e "\nRemove '${INPUT_BRANCH}' pull request review protection ..." - push-action --token "${INPUT_TOKEN}" --ref "${INPUT_BRANCH}" --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" -- unprotect_reviews + + push-action \ + --token "${INPUT_TOKEN}" \ + --ref "${INPUT_BRANCH}" \ + --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" \ + -- unprotect_reviews + echo "Remove '${INPUT_BRANCH}' pull request review protection ... DONE!" fi ;; @@ -33,7 +39,13 @@ protect () { y | Y | yes | Yes | YES | true | True | TRUE | on | On | ON) if [ -n "${PUSH_PROTECTED_CHANGED_BRANCH}" ] && [ -n "${PUSH_PROTECTED_PROTECTED_BRANCH}" ]; then echo -e "\nRe-add '${INPUT_BRANCH}' pull request review protection ..." - push-action --token "${INPUT_TOKEN}" --ref "${INPUT_BRANCH}" --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" -- protect_reviews + + push-action \ + --token "${INPUT_TOKEN}" \ + --ref "${INPUT_BRANCH}" \ + --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" \ + -- protect_reviews + echo "Re-add '${INPUT_BRANCH}' pull request review protection ... DONE!" fi ;; @@ -49,14 +61,38 @@ wait_for_checks() { echo -e "\nWaiting for status checks to finish for '${PUSH_PROTECTED_TEMPORARY_BRANCH}' ..." # Sleep for 5 seconds to let the workflows start sleep ${INPUT_SLEEP} - push-action --token "${INPUT_TOKEN}" --ref "${INPUT_BRANCH}" --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" --wait-timeout "${INPUT_TIMEOUT}" --wait-interval "${INPUT_INTERVAL}" -- wait_for_checks + + ACCEPTABLE_CONCLUSIONS=() + if [ -n "${INPUT_ACCEPTABLE_CONCLUSIONS}" ]; then + while IFS="," read -ra CONCLUSIONS; do + for CONCLUSION in "${CONCLUSIONS[@]}"; do + ACCEPTABLE_CONCLUSIONS+=(--acceptable-conclusion="${CONCLUSION}") + done + done <<< "${INPUT_ACCEPTABLE_CONCLUSIONS}" + fi + + push-action \ + --token "${INPUT_TOKEN}" \ + --ref "${INPUT_BRANCH}" \ + --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" \ + --wait-timeout "${INPUT_TIMEOUT}" \ + --wait-interval "${INPUT_INTERVAL}"\ + "${ACCEPTABLE_CONCLUSIONS[@]}" \ + -- wait_for_checks + echo "Waiting for status checks to finish for '${PUSH_PROTECTED_TEMPORARY_BRANCH}' ... DONE!" fi } remove_remote_temp_branch() { if [ -n "${PUSH_PROTECTED_CHANGED_BRANCH}" ] && [ -n "${PUSH_PROTECTED_PROTECTED_BRANCH}" ]; then echo -e "\nRemoving temporary branch '${PUSH_PROTECTED_TEMPORARY_BRANCH}' ..." - push-action --token "${INPUT_TOKEN}" --ref "${INPUT_BRANCH}" --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" -- remove_temp_branch + + push-action \ + --token "${INPUT_TOKEN}" \ + --ref "${INPUT_BRANCH}" \ + --temp-branch "${PUSH_PROTECTED_TEMPORARY_BRANCH}" \ + -- remove_temp_branch + echo "Removing temporary branch '${PUSH_PROTECTED_TEMPORARY_BRANCH}' ... DONE!" fi } diff --git a/push_action/run.py b/push_action/run.py index e19088d..a7162f0 100644 --- a/push_action/run.py +++ b/push_action/run.py @@ -19,8 +19,9 @@ Match found required GitHub Actions runs found in 1) 4) Wait and do 3) again until required GitHub Actions jobs have "status": "completed" - If "conclusion": "success" YAY - If "conclusion" != "success" FAIL this action + If "conclusion" in inputs provided through `--acceptable-conclusion` + (default: "success") YAY + Otherwise, FAIL this action """ import argparse @@ -39,8 +40,9 @@ get_workflow_run_jobs, remove_branch, ) +from push_action.validate import validate_conclusions -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from typing import Any, Dict @@ -75,7 +77,10 @@ def wait() -> None: # All jobs are completed print("All required GitHub Actions jobs complete!", flush=True) unsuccessful_jobs = [ - _ for _ in actions_required if _.get("conclusion", "") != "success" + job_run + for job_run in actions_required + if job_run.get("conclusion", "") + not in IN_MEMORY_CACHE["acceptable_conclusions"] ] break @@ -95,6 +100,7 @@ def wait() -> None: if _["name"] in required_statuses and _["status"] != "completed" ] ) + if actions_required: print( f"{len(actions_required)} required GitHub Actions jobs have not yet " @@ -237,6 +243,16 @@ def main() -> None: ), default=30, ) + parser.add_argument( + "--acceptable-conclusion", + type=str, + help=( + "Acceptable conclusion for the wait_for_checks run to be considered " + "successful" + ), + action="append", + default=["success"], + ) parser.add_argument( "ACTION", type=str, @@ -254,6 +270,10 @@ def main() -> None: fail = "" try: + IN_MEMORY_CACHE["acceptable_conclusions"] = validate_conclusions( + IN_MEMORY_CACHE["args"].acceptable_conclusion + ) + if IN_MEMORY_CACHE["args"].ACTION == "wait_for_checks": wait() elif IN_MEMORY_CACHE["args"].ACTION == "remove_temp_branch": @@ -266,10 +286,8 @@ def main() -> None: print(protected_branch(IN_MEMORY_CACHE["args"].ref), end="", flush=True) else: raise RuntimeError(f"Unknown ACTIONS {IN_MEMORY_CACHE['args'].ACTION!r}") + except Exception as exc: # pylint: disable=broad-except fail = f"{exc.__class__.__name__}: {exc}" - if fail: - sys.exit(fail) - else: - sys.exit() + sys.exit(fail or None) diff --git a/push_action/utils.py b/push_action/utils.py index 2ee3dd9..41d4dcc 100644 --- a/push_action/utils.py +++ b/push_action/utils.py @@ -19,7 +19,7 @@ from push_action.cache import IN_MEMORY_CACHE if TYPE_CHECKING: - from typing import Callable, List, Optional, Union + from typing import Callable, List, Union REQUEST_TIMEOUT = 10 # in seconds diff --git a/push_action/validate.py b/push_action/validate.py new file mode 100644 index 0000000..464b51a --- /dev/null +++ b/push_action/validate.py @@ -0,0 +1,42 @@ +"""Validate inputs.""" +from __future__ import annotations + + +VALID_CONCLUSIONS = [ + "action_required", + "cancelled", + "failure", + "neutral", + "skipped", + "success", + "timed_out", +] +"""List of valid GitHub Actions workflow job run conclusions. +This is taken from +https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2022-11-28#get-a-job-for-a-workflow-run +as of 30.10.2023. +""" + + +def validate_conclusions(conclusions: list[str]) -> list[str]: + """Validate the conclusions. + + I.e., ensure they are valid GitHub Actions workflow job run conclusions. + """ + if not conclusions: + raise ValueError( + "No conclusions supplied - at least one is required (default: 'success')." + ) + + for conclusion in conclusions: + invalid_conclusions: list[str] = [] + if conclusion not in VALID_CONCLUSIONS: + invalid_conclusions.append(conclusion) + + if invalid_conclusions: + return [ + f"Invalid supplied conclusions: {invalid_conclusions}. " + f"Valid conclusions are: {VALID_CONCLUSIONS}" + ] + + return conclusions