From 76ce58dc6ea2d9884ed02ded36316caf12b6380e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 29 Dec 2023 10:02:16 -0500 Subject: [PATCH 1/6] CI: Add a verify success job to use in required checks --- .github/workflows/python-code-quality.yml | 15 ++- .github/workflows/verify-success.yml | 121 ++++++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/verify-success.yml diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index ccfe46c5758..25188b82e72 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -13,11 +13,10 @@ on: jobs: python-checks: - name: Python Code Quality + name: Python Code Quality Checks concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ - matrix.pylint-version }} + group: ${{ github.workflow }}-${{ github.job }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ matrix.pylint-version }} cancel-in-progress: true # Using matrix just to get variables which are not environmental variables @@ -45,11 +44,11 @@ jobs: echo Pylint: ${{ matrix.pylint-version }} - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: pip - name: Install non-Python dependencies run: | @@ -146,3 +145,11 @@ jobs: name: sphinx-grass path: sphinx-grass retention-days: 3 + python-success: + name: Python Code Quality Result + needs: + - python-checks + if: ${{ always() }} + uses: ./.github/workflows/verify-success.yml + with: + needs_context: ${{ toJson(needs) }} diff --git a/.github/workflows/verify-success.yml b/.github/workflows/verify-success.yml new file mode 100644 index 00000000000..98c68651650 --- /dev/null +++ b/.github/workflows/verify-success.yml @@ -0,0 +1,121 @@ +--- +name: Verify Success reusable workflow + +# Use this reusable workflow as a job of workflow to check that +# all jobs, including ones ran through a matrix, were successful. +# This job can then be used as a required status check in the +# repo's rulesets, that allows to change the required jobs or +# the matrix values of a required job without needing to change +# the rulesets settings. In the future, GitHub might have a +# solution to this natively. + +# This reusable workflow has inputs to change what is required +# to have this workflow pass. It handles the cases were there were +# skipped jobs, and no successful jobs. + +# The jobs to check must set as the `needs` for the job calling this +# reusable workflow. This also means that the job ids should be in the +# same workflow file. The calling job must be set to always run to be +# triggered when jobs are skipped or cancelled. +# Then, set the `needs_context` input like: +# `needs_context: ${{ toJson(needs) }}` + +# Example usage, as a job inside a workflow: +# ``` +# jobs: +# a-job-id: +# ... +# another-job-id: +# ... +# some-job-success: +# name: Some Job Result +# needs: +# - a-job-id +# - another-job-id +# if: ${{ always() }} +# uses: ./.github/workflows/verify-success.yml +# with: +# needs_context: ${{ toJson(needs) }} +# ``` + +on: + workflow_call: + inputs: + needs_context: + type: string + required: true + # Can't escape the handlebars in the description + description: In the calling job that defines all the needed jobs, send `toJson(needs)` inside `$` followed by `{{ }}`. + fail_if_failure: + type: boolean + default: true + description: If true, this workflow will fail if any job from 'needs_context was failed + fail_if_cancelled: + type: boolean + default: true + description: If true, this workflow will fail if any job from 'needs_context' was cancelled + fail_if_skipped: + type: boolean + default: false + description: If true, this workflow will fail if any job from 'needs_context' was skipped + require_success: + type: boolean + default: true + description: If true, this workflow will fail if no job from 'needs_context' was successful + +jobs: + verify-success: + name: Success + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Set outputs for each job result type + id: has-result + run: | + echo "failure=${{ contains(env.NEEDS_RESULT, 'failure') }}" >> "$GITHUB_OUTPUT" + echo "cancelled=${{ contains(env.NEEDS_RESULT, 'cancelled') }}" >> "$GITHUB_OUTPUT" + echo "skipped=${{ contains(env.NEEDS_RESULT, 'skipped') }}" >> "$GITHUB_OUTPUT" + echo "success=${{ contains(env.NEEDS_RESULT, 'success') }}" >> "$GITHUB_OUTPUT" + env: + NEEDS_RESULT: ${{ toJson(fromJson(inputs.needs_context).*.result) }} + - name: Set exit codes for each job result type + id: exit-code + run: | + echo "failure=${{ inputs.fail_if_failure && fromJson(steps.has-result.outputs.failure) && 1 || 0 }}" >> "$GITHUB_OUTPUT" + echo "cancelled=${{ inputs.fail_if_cancelled && fromJson(steps.has-result.outputs.cancelled) && 1 || 0 }}" >> "$GITHUB_OUTPUT" + echo "skipped=${{ inputs.fail_if_skipped && fromJson(steps.has-result.outputs.skipped) && 1 || 0 }}" >> "$GITHUB_OUTPUT" + echo "success=${{ inputs.require_success && !fromJson(steps.has-result.outputs.success) && 1 || 0 }}" >> "$GITHUB_OUTPUT" + - name: Set messages for each job result type + id: message + run: | + echo "failure=${{ format('{0}{1} were failed', + (steps.exit-code.outputs.failure == 1) && env.P1 || env.P2, + (steps.has-result.outputs.failure == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" + echo "cancelled=${{ format('{0}{1} were cancelled', + (steps.exit-code.outputs.cancelled == 1) && env.P1 || env.P2, + (steps.has-result.outputs.cancelled == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" + echo "skipped=${{ format('{0}{1} were skipped', + (steps.exit-code.outputs.skipped == 1) && env.P1 || env.P2, + (steps.has-result.outputs.skipped == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" + echo "success=${{ format('{0}{1} were successful', + (steps.exit-code.outputs.success == 1) && env.P1 || env.P2, + (steps.has-result.outputs.success == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" + env: + P1: "::error ::" # Common message prefix if step will fail + P2: "" # Common message prefix if step will not fail + M1: "Some jobs" # Common message if result is true + M2: "No jobs" # Common message if result is false + + - name: Check for failed jobs + run: echo "${{ steps.message.outputs.failure }}" && exit ${{ steps.exit-code.outputs.failure }} + - name: Check for cancelled jobs + run: echo "${{ steps.message.outputs.cancelled }}" && exit ${{ steps.exit-code.outputs.cancelled }} + - name: Check for skipped jobs + run: echo "${{ steps.message.outputs.skipped }}" && exit ${{ steps.exit-code.outputs.skipped }} + - name: Check for successful jobs + run: echo "${{ steps.message.outputs.success }}" && exit ${{ steps.exit-code.outputs.success }} + + - run: echo "Checks passed successfully" + if: ${{ success() }} + - run: echo "Checks failed" + if: ${{ !success() }} From d3f03d0f0106827e38749c1f427dac74bd0a5965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:58:16 +0000 Subject: [PATCH 2/6] Limit line length in verify-success.yml and Python Code Quality workflow --- .github/workflows/python-code-quality.yml | 6 +- .github/workflows/verify-success.yml | 92 ++++++++++++++++------- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index 25188b82e72..68e1a2827ae 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -16,7 +16,9 @@ jobs: name: Python Code Quality Checks concurrency: - group: ${{ github.workflow }}-${{ github.job }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ matrix.pylint-version }} + group: ${{ github.workflow }}-${{ github.job }}-${{ + github.event_name == 'pull_request' && + github.head_ref || github.sha }}-${{ matrix.pylint-version }} cancel-in-progress: true # Using matrix just to get variables which are not environmental variables @@ -55,7 +57,7 @@ jobs: sudo apt-get update -y sudo apt-get install -y wget git gawk findutils xargs -a <(awk '! /^ *(#|$)/' ".github/workflows/apt.txt") -r -- \ - sudo apt-get install -y --no-install-recommends --no-install-suggests + sudo apt-get install -y --no-install-recommends --no-install-suggests - name: Install Python dependencies run: | diff --git a/.github/workflows/verify-success.yml b/.github/workflows/verify-success.yml index 98c68651650..8ebeb7c4665 100644 --- a/.github/workflows/verify-success.yml +++ b/.github/workflows/verify-success.yml @@ -45,23 +45,33 @@ on: type: string required: true # Can't escape the handlebars in the description - description: In the calling job that defines all the needed jobs, send `toJson(needs)` inside `$` followed by `{{ }}`. + description: + In the calling job that defines all the needed jobs, + send `toJson(needs)` inside `$` followed by `{{ }}` fail_if_failure: type: boolean default: true - description: If true, this workflow will fail if any job from 'needs_context was failed + description: + If true, this workflow will fail if any job from 'needs_context was + failed fail_if_cancelled: type: boolean default: true - description: If true, this workflow will fail if any job from 'needs_context' was cancelled + description: + If true, this workflow will fail if any job from 'needs_context' was + cancelled fail_if_skipped: type: boolean default: false - description: If true, this workflow will fail if any job from 'needs_context' was skipped + description: + If true, this workflow will fail if any job from 'needs_context' was + skipped require_success: type: boolean default: true - description: If true, this workflow will fail if no job from 'needs_context' was successful + description: + If true, this workflow will fail if no job from 'needs_context' was + successful jobs: verify-success: @@ -72,34 +82,50 @@ jobs: - name: Set outputs for each job result type id: has-result run: | - echo "failure=${{ contains(env.NEEDS_RESULT, 'failure') }}" >> "$GITHUB_OUTPUT" - echo "cancelled=${{ contains(env.NEEDS_RESULT, 'cancelled') }}" >> "$GITHUB_OUTPUT" - echo "skipped=${{ contains(env.NEEDS_RESULT, 'skipped') }}" >> "$GITHUB_OUTPUT" - echo "success=${{ contains(env.NEEDS_RESULT, 'success') }}" >> "$GITHUB_OUTPUT" + echo "failure=${{ + contains(env.NEEDS_RESULT, 'failure') }}" >> "$GITHUB_OUTPUT" + echo "cancelled=${{ + contains(env.NEEDS_RESULT, 'cancelled') }}" >> "$GITHUB_OUTPUT" + echo "skipped=${{ + contains(env.NEEDS_RESULT, 'skipped') }}" >> "$GITHUB_OUTPUT" + echo "success=${{ + contains(env.NEEDS_RESULT, 'success') }}" >> "$GITHUB_OUTPUT" env: NEEDS_RESULT: ${{ toJson(fromJson(inputs.needs_context).*.result) }} - name: Set exit codes for each job result type id: exit-code run: | - echo "failure=${{ inputs.fail_if_failure && fromJson(steps.has-result.outputs.failure) && 1 || 0 }}" >> "$GITHUB_OUTPUT" - echo "cancelled=${{ inputs.fail_if_cancelled && fromJson(steps.has-result.outputs.cancelled) && 1 || 0 }}" >> "$GITHUB_OUTPUT" - echo "skipped=${{ inputs.fail_if_skipped && fromJson(steps.has-result.outputs.skipped) && 1 || 0 }}" >> "$GITHUB_OUTPUT" - echo "success=${{ inputs.require_success && !fromJson(steps.has-result.outputs.success) && 1 || 0 }}" >> "$GITHUB_OUTPUT" + echo "failure=${{ inputs.fail_if_failure && + fromJson(steps.has-result.outputs.failure) && 1 || 0 + }}" >> "$GITHUB_OUTPUT" + echo "cancelled=${{ inputs.fail_if_cancelled && + fromJson(steps.has-result.outputs.cancelled) && 1 || 0 + }}" >> "$GITHUB_OUTPUT" + echo "skipped=${{ inputs.fail_if_skipped && + fromJson(steps.has-result.outputs.skipped) && 1 || 0 + }}" >> "$GITHUB_OUTPUT" + echo "success=${{ inputs.require_success && + !fromJson(steps.has-result.outputs.success) && 1 || 0 + }}" >> "$GITHUB_OUTPUT" - name: Set messages for each job result type id: message run: | - echo "failure=${{ format('{0}{1} were failed', - (steps.exit-code.outputs.failure == 1) && env.P1 || env.P2, - (steps.has-result.outputs.failure == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" - echo "cancelled=${{ format('{0}{1} were cancelled', - (steps.exit-code.outputs.cancelled == 1) && env.P1 || env.P2, - (steps.has-result.outputs.cancelled == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" - echo "skipped=${{ format('{0}{1} were skipped', - (steps.exit-code.outputs.skipped == 1) && env.P1 || env.P2, - (steps.has-result.outputs.skipped == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" - echo "success=${{ format('{0}{1} were successful', - (steps.exit-code.outputs.success == 1) && env.P1 || env.P2, - (steps.has-result.outputs.success == 'true') && env.M1 || env.M2) }}" >> "$GITHUB_OUTPUT" + echo "failure=${{ format('{0}{1} were failed', + (steps.exit-code.outputs.failure == 1) && env.P1 || env.P2, + (steps.has-result.outputs.failure == 'true') && env.M1 || env.M2) + }}" >> "$GITHUB_OUTPUT" + echo "cancelled=${{ format('{0}{1} were cancelled', + (steps.exit-code.outputs.cancelled == 1) && env.P1 || env.P2, + (steps.has-result.outputs.cancelled == 'true') && env.M1 || env.M2) + }}" >> "$GITHUB_OUTPUT" + echo "skipped=${{ format('{0}{1} were skipped', + (steps.exit-code.outputs.skipped == 1) && env.P1 || env.P2, + (steps.has-result.outputs.skipped == 'true') && env.M1 || env.M2) + }}" >> "$GITHUB_OUTPUT" + echo "success=${{ format('{0}{1} were successful', + (steps.exit-code.outputs.success == 1) && env.P1 || env.P2, + (steps.has-result.outputs.success == 'true') && env.M1 || env.M2) + }}" >> "$GITHUB_OUTPUT" env: P1: "::error ::" # Common message prefix if step will fail P2: "" # Common message prefix if step will not fail @@ -107,13 +133,21 @@ jobs: M2: "No jobs" # Common message if result is false - name: Check for failed jobs - run: echo "${{ steps.message.outputs.failure }}" && exit ${{ steps.exit-code.outputs.failure }} + run: | + echo "${{ steps.message.outputs.failure }}" + exit ${{ steps.exit-code.outputs.failure }} - name: Check for cancelled jobs - run: echo "${{ steps.message.outputs.cancelled }}" && exit ${{ steps.exit-code.outputs.cancelled }} + run: | + echo "${{ steps.message.outputs.cancelled }}" + exit ${{ steps.exit-code.outputs.cancelled }} - name: Check for skipped jobs - run: echo "${{ steps.message.outputs.skipped }}" && exit ${{ steps.exit-code.outputs.skipped }} + run: | + echo "${{ steps.message.outputs.skipped }}" + exit ${{ steps.exit-code.outputs.skipped }} - name: Check for successful jobs - run: echo "${{ steps.message.outputs.success }}" && exit ${{ steps.exit-code.outputs.success }} + run: | + echo "${{ steps.message.outputs.success }}" + exit ${{ steps.exit-code.outputs.success }} - run: echo "Checks passed successfully" if: ${{ success() }} From 78634f437e9683dc6abf285e63fffdf4b407e602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:12:26 +0000 Subject: [PATCH 3/6] CI: Use pipx to run tools in python-code-quality.yml, cache pip --- .github/workflows/python-code-quality.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index 68e1a2827ae..e41090c2cec 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -27,11 +27,11 @@ jobs: matrix: include: - os: ubuntu-22.04 - python-version: '3.10' - min-python-version: '3.7' - black-version: '23.1.0' - flake8-version: '3.9.2' - pylint-version: '2.12.2' + python-version: "3.10" + min-python-version: "3.7" + black-version: "23.1.0" + flake8-version: "3.9.2" + pylint-version: "2.12.2" runs-on: ${{ matrix.os }} @@ -40,12 +40,13 @@ jobs: run: | echo OS: ${{ matrix.os }} echo Python: ${{ matrix.python-version }} - echo Minimimal Python version: ${{ matrix.min-python-version }} + echo Minimal Python version: ${{ matrix.min-python-version }} echo Black: ${{ matrix.black-version }} echo Flake8: ${{ matrix.flake8-version }} echo Pylint: ${{ matrix.pylint-version }} - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: Set up Python uses: actions/setup-python@v5 with: @@ -64,9 +65,11 @@ jobs: python -m pip install --upgrade pip pip install -r .github/workflows/python_requirements.txt pip install -r .github/workflows/optional_requirements.txt - pip install black==${{ matrix.black-version }} - pip install flake8==${{ matrix.flake8-version }} - pip install pylint==${{ matrix.pylint-version }} pytest-github-actions-annotate-failures + pip install pipx \ + pylint==${{ matrix.pylint-version }} \ + pytest-github-actions-annotate-failures + pipx install black[jupyter]==${{ matrix.black-version }} + pipx install flake8==${{ matrix.flake8-version }} - name: Run Black run: | From 1728e462beb018e706e9fe3262766927227eed6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 29 Dec 2023 16:27:10 +0000 Subject: [PATCH 4/6] CI: Add verify success for pytest workflow --- .github/workflows/pytest.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 8d470934713..9b8db8ae440 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -80,3 +80,11 @@ jobs: - name: Print installed versions if: always() run: .github/workflows/print_versions.sh + pytest-success: + name: pytest Result + needs: + - pytest + if: ${{ always() }} + uses: ./.github/workflows/verify-success.yml + with: + needs_context: ${{ toJson(needs) }} From 0a7832563044aaca5ac4648378e2a854dead069a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 29 Dec 2023 16:27:37 +0000 Subject: [PATCH 5/6] CI: Add verify-success to Ubuntu --- .github/workflows/ubuntu.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index f53ed4d2a3c..032234db885 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -14,7 +14,7 @@ on: - releasebranch_* jobs: - build-and-test: + ubuntu: name: ${{ matrix.name }} tests concurrency: @@ -26,7 +26,7 @@ jobs: strategy: matrix: include: - - name: '22.04' + - name: "22.04" os: ubuntu-22.04 config: ubuntu-22.04 # This is without optional things but it still keeps things useful, @@ -95,3 +95,11 @@ jobs: - name: Print installed versions if: always() run: .github/workflows/print_versions.sh + build-and-test-success: + name: Build & Test Result + needs: + - ubuntu + if: ${{ always() }} + uses: ./.github/workflows/verify-success.yml + with: + needs_context: ${{ toJson(needs) }} From f9842168a3288291811812657ca98741c6d4da02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 30 Dec 2023 16:25:24 -0500 Subject: [PATCH 6/6] Update .github/workflows/python-code-quality.yml --- .github/workflows/python-code-quality.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index e41090c2cec..4c872e5df87 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -65,11 +65,9 @@ jobs: python -m pip install --upgrade pip pip install -r .github/workflows/python_requirements.txt pip install -r .github/workflows/optional_requirements.txt - pip install pipx \ - pylint==${{ matrix.pylint-version }} \ - pytest-github-actions-annotate-failures - pipx install black[jupyter]==${{ matrix.black-version }} - pipx install flake8==${{ matrix.flake8-version }} + pip install black==${{ matrix.black-version }} + pip install flake8==${{ matrix.flake8-version }} + pip install pylint==${{ matrix.pylint-version }} pytest-github-actions-annotate-failures - name: Run Black run: |