From 8bee2f89494db5347f966ae569c93746cba61fd3 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:24:09 +0300 Subject: [PATCH 1/7] feat(action): enhance GitHub action - change to working directory - install `pip-requirements-parser` in a separate step - include Darker exit code in outputs - include Darker standard output in outputs - run `commit-range` action locally - caching of the virtualenv and pip downloads --- action.yml | 37 ++++++++++++++++++++++++++++- action/main.py | 63 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/action.yml b/action.yml index cd1196805..b7e43e56e 100644 --- a/action.yml +++ b/action.yml @@ -27,6 +27,19 @@ inputs: NOTE: Baseline linting has been moved to the Graylint package. required: false default: '' + working-directory: + description: >- + Directory to run Darker in, either absolute or relative to + $GITHUB_WORKSPACE. By default, Darker is run in $GITHUB_WORKSPACE. + required: false + default: '.' +outputs: + exitcode: + description: "Exit code of Darker" + value: ${{ steps.darker.outputs.exitcode }} + stdout: + description: "Standard output of Darker" + value: ${{ steps.darker.outputs.stdout }} branding: color: "black" icon: "check-circle" @@ -35,8 +48,29 @@ runs: steps: - name: Commit Range id: commit-range - uses: akaihola/darker/.github/actions/commit-range@1.7.1 + uses: ./.github/actions/commit-range + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Get pip cache dir + id: pip-cache + run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + shell: bash + - name: Cache Darker environment + uses: actions/cache@v4 + with: + path: | + ${{ github.action_path }}/.darker-env + ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-darker-${{ inputs.version }}-lint-${{ inputs.lint }} + save-always: true + - name: Install dependencies + run: | + pip install pip-requirements-parser + shell: bash - name: Run Darker + id: darker run: | # Exists since using github.action_path + path to main script doesn't # work because bash interprets the backslashes in github.action_path @@ -68,5 +102,6 @@ runs: INPUT_REVISION: ${{ inputs.revision }} INPUT_LINT: ${{ inputs.lint }} INPUT_COMMIT_RANGE: ${{ steps.commit-range.outputs.commit-range }} + INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }} pythonioencoding: utf-8 shell: bash diff --git a/action/main.py b/action/main.py index d4cd0f988..52b33284a 100644 --- a/action/main.py +++ b/action/main.py @@ -13,6 +13,7 @@ SRC = os.getenv("INPUT_SRC", default="") VERSION = os.getenv("INPUT_VERSION", default="") REVISION = os.getenv("INPUT_REVISION") or os.getenv("INPUT_COMMIT_RANGE") or "HEAD^" +WORKING_DIRECTORY = os.getenv("INPUT_WORKING_DIRECTORY", ".") if os.getenv("INPUT_LINT", default=""): print( @@ -20,28 +21,60 @@ " See https://pypi.org/project/graylint for more information.", ) -run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True) # nosec + +def set_github_output(key: str, val: str) -> None: + """Write a key-value pair to the output file.""" + with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="UTF-8") as f: + if "\n" in val: + print(f"{key}< None: + """Write the exit code to the output file and exit with it.""" + set_github_output("exitcode", str(exitcode)) + sys.exit(exitcode) + + +# Check if the working directory exists +if not os.path.isdir(WORKING_DIRECTORY): + print(f"::error::Working directory does not exist: {WORKING_DIRECTORY}", flush=True) + exit_with_exitcode(21) + + +def pip_install(*packages): + """Install the specified Python packages using a pip subprocess.""" + python = str(ENV_BIN / "python") + args = [python, "-m", "pip", "install", *packages] + pip_proc = run( # nosec + args, + check=False, + stdout=PIPE, + stderr=STDOUT, + encoding="utf-8", + ) + print(pip_proc.stdout, end="") + if pip_proc.returncode: + print(f"::error::Failed to install {' '.join(packages)}.", flush=True) + sys.exit(pip_proc.returncode) + + +if not ENV_PATH.exists(): + run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True) # nosec req = ["darker[black,color,isort]"] if VERSION: if VERSION.startswith("@"): - req[0] = f"git+https://github.com/akaihola/darker{VERSION}#egg={req[0]}" + req[0] = f"git+https://github.com/akaihola/darker{VERSION}#egg=darker" elif VERSION.startswith(("~", "=", "<", ">")): req[0] += VERSION else: req[0] += f"=={VERSION}" -pip_proc = run( # nosec - [str(ENV_BIN / "python"), "-m", "pip", "install"] + req, - check=False, - stdout=PIPE, - stderr=STDOUT, - encoding="utf-8", -) -print(pip_proc.stdout, end="") -if pip_proc.returncode: - print(f"::error::Failed to install {' '.join(req)}.", flush=True) - sys.exit(pip_proc.returncode) +pip_install(*req) base_cmd = [str(ENV_BIN / "darker")] @@ -58,7 +91,9 @@ stderr=STDOUT, env={**os.environ, "PATH": f"{ENV_BIN}:{os.environ['PATH']}"}, encoding="utf-8", + cwd=WORKING_DIRECTORY, ) print(proc.stdout, end="") -sys.exit(proc.returncode) +set_github_output("stdout", proc.stdout) +exit_with_exitcode(proc.returncode) From b1d6393ba070766f662c75df1da4b5b98e45fd10 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 27 Jul 2024 21:16:36 +0300 Subject: [PATCH 2/7] build(action): test `working-directory` input --- .github/workflows/test-working-directory.yml | 107 +++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/test-working-directory.yml diff --git a/.github/workflows/test-working-directory.yml b/.github/workflows/test-working-directory.yml new file mode 100644 index 000000000..27630ad6b --- /dev/null +++ b/.github/workflows/test-working-directory.yml @@ -0,0 +1,107 @@ +--- +name: "GH Action `working-directory:` test" + +on: push # yamllint disable-line rule:truthy + +jobs: + setup: + runs-on: ubuntu-latest + steps: + - name: Set up initial test repository + run: | + mkdir -p test-repo + cd test-repo + git init + git config user.email "test@example.com" + git config user.name "Test User" + git commit --allow-empty -m "Initial empty commit" + echo 'def hello(): return "Hello, World!"' > test.py + git add test.py + git commit -m "Add test.py file" + echo 'def hello(): return "Hello, World!"' > test.py + + - name: Upload test repository + uses: actions/upload-artifact@v3 + with: + name: test-repo + path: test-repo + + test: + needs: setup + runs-on: ubuntu-latest + strategy: + matrix: + scenario: + - name: no-work-dir + working_directory: null # Consider it omitted + src: test.py + options: --check --diff + expected_exitcode: 2 + - name: right-workdir + working_directory: test-repo + src: test.py + options: --check --diff + expected_exitcode: 1 + - name: wrong-work-dir + working_directory: non-existent-dir + src: test.py + options: --check --diff + expected_exitcode: 21 + - name: file-not-found + working_directory: test-repo + src: non_existent_file.py + options: --check --diff + expected_exitcode: 2 + - name: invalid-arguments + working_directory: test-repo + src: test.py + options: --invalid-option + expected_exitcode: 3 + - name: missing-deps + working_directory: test-repo + src: test.py + options: --flynt # not yet supported by the action + expected_exitcode: 4 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 10 + + - name: Download test repository + uses: actions/download-artifact@v3 + with: + name: test-repo + path: ${{ runner.temp }}/test-repo + + - name: "Run Darker - ${{ matrix.scenario.name }}" + uses: ./ + continue-on-error: true + id: darker-run + with: + version: "@gh-action-working-directory" + options: ${{ matrix.scenario.options }} + # In the action, '.' is the default value used if `working-directory` omitted + working-directory: >- + ${{ + matrix.scenario.working_directory == null && '.' + || format('{0}/{1}', runner.temp, matrix.scenario.working_directory) + }} + src: ${{ matrix.scenario.src }} + revision: HEAD + + - name: Check exit code + if: >- + steps.darker-run.outputs.exitcode != matrix.scenario.expected_exitcode + run: | + echo "::error::Expected exit code ${{ matrix.scenario.expected_exitcode }}" + echo "::error::Darker exited with ${{ steps.darker-run.outputs.exitcode }}" + exit 1 + + - name: Verify diff output for right-workdir scenario + if: > + matrix.scenario.name == 'right-workdir' && + !contains(steps.darker-run.outputs.stdout, '@@') + run: | + echo "::error::Darker did not output a diff as expected" + exit 1 From 1ece4e93b5b1cd6c83e7316bdbcce08f8b9a0791 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:10:25 +0300 Subject: [PATCH 3/7] test(action): fix GitHub action tests --- action/tests/test_main.py | 44 ++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/action/tests/test_main.py b/action/tests/test_main.py index 9a10e684a..17aad68de 100644 --- a/action/tests/test_main.py +++ b/action/tests/test_main.py @@ -49,7 +49,9 @@ def patch_main( def run(args, **kwargs): returncode = pip_returncode if args[1:3] == ["-m", "pip"] else 0 - return CompletedProcess(args, returncode, stdout="", stderr="") + return CompletedProcess( + args, returncode, stdout="Output\nfrom\nDarker", stderr="" + ) run_mock = Mock(wraps=run) exit_ = Mock(side_effect=SysExitCalled) @@ -78,7 +80,16 @@ def main_patch( yield run_main_fixture -def test_creates_virtualenv(tmp_path, main_patch): +@pytest.fixture +def github_output(tmp_path: Path) -> Generator[Path]: + """Fixture to set up a GitHub output file for the action""" + gh_output_filepath = tmp_path / "github.output" + with patch.dict("os.environ", {"GITHUB_OUTPUT": str(gh_output_filepath)}): + + yield gh_output_filepath + + +def test_creates_virtualenv(tmp_path, main_patch, github_output): """The GitHub action creates a virtualenv for Darker""" with pytest.raises(SysExitCalled): @@ -99,8 +110,7 @@ def test_creates_virtualenv(tmp_path, main_patch): dict( run_main_env={"INPUT_VERSION": "@master"}, expect=[ - "git+https://github.com/akaihola/darker" - "@master#egg=darker[black,color,isort]" + "darker[black,color,isort]@git+https://github.com/akaihola/darker@master" ], ), dict( @@ -112,7 +122,7 @@ def test_creates_virtualenv(tmp_path, main_patch): expect=["darker[black,color,isort]"], ), ) -def test_installs_packages(tmp_path, main_patch, run_main_env, expect): +def test_installs_packages(tmp_path, main_patch, github_output, run_main_env, expect): """Darker, isort and linters are installed in the virtualenv using pip""" with pytest.raises(SysExitCalled): @@ -185,7 +195,12 @@ def test_installs_packages(tmp_path, main_patch, run_main_env, expect): ], ), ) -def test_runs_darker(tmp_path: Path, env: dict[str, str], expect: list[str]) -> None: +def test_runs_darker( + tmp_path: Path, + github_output: Generator[Path], + env: dict[str, str], + expect: list[str], +) -> None: """Configuration translates correctly into a Darker command line""" with patch_main(tmp_path, env) as main_patch, pytest.raises(SysExitCalled): @@ -218,15 +233,28 @@ def test_error_if_pip_fails(tmp_path, capsys): ) assert ( capsys.readouterr().out.splitlines()[-1] - == "::error::Failed to install darker[black,color,isort]." + == "Darker::error::Failed to install darker[black,color,isort]." ) main_patch.sys.exit.assert_called_once_with(42) -def test_exits(main_patch): +def test_exits(main_patch, github_output): """A successful run exits with a zero return code""" with pytest.raises(SysExitCalled): run_module("main") main_patch.sys.exit.assert_called_once_with(0) + + +@pytest.mark.parametrize( + "expect_line", + ["exitcode=0", "stdout< Date: Wed, 31 Jul 2024 00:22:56 +0300 Subject: [PATCH 4/7] fix(action): install Darker w/ extras in GH action --- action/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/action/main.py b/action/main.py index 52b33284a..9572dd8d8 100644 --- a/action/main.py +++ b/action/main.py @@ -68,7 +68,9 @@ def pip_install(*packages): req = ["darker[black,color,isort]"] if VERSION: if VERSION.startswith("@"): - req[0] = f"git+https://github.com/akaihola/darker{VERSION}#egg=darker" + req[0] = ( + f"git+https://github.com/akaihola/darker{VERSION}[color,isort]#egg=darker" + ) elif VERSION.startswith(("~", "=", "<", ">")): req[0] += VERSION else: From 5a5ea90ae355ae39c3c4f1eaa3619a51b32fbd26 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:59:13 +0300 Subject: [PATCH 5/7] fix(action): use PEP 440 syntax to install Darker --- action/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/action/main.py b/action/main.py index 9572dd8d8..e2a9ceab3 100644 --- a/action/main.py +++ b/action/main.py @@ -68,9 +68,7 @@ def pip_install(*packages): req = ["darker[black,color,isort]"] if VERSION: if VERSION.startswith("@"): - req[0] = ( - f"git+https://github.com/akaihola/darker{VERSION}[color,isort]#egg=darker" - ) + req[0] += f"@git+https://github.com/akaihola/darker{VERSION}" elif VERSION.startswith(("~", "=", "<", ">")): req[0] += VERSION else: From 3e2b06da68ce4d8d79a71d34fb49b445c90b0aac Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:02:13 +0300 Subject: [PATCH 6/7] docs: update the change log --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3b92b56cd..449bf328f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,6 +45,7 @@ Fixed The work-around should be removed when Python 3.8 and 3.9 are no longer supported. - Add missing configuration flag for Flynt_. - Only split source code lines at Python's universal newlines (LF, CRLF, CR). +- The Darker GitHub action now respects the ``working-directory`` input option. 2.1.1_ - 2024-04-16 From 33eb979eb17fd2d572e46242863648e65dadcb69 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:53:30 +0200 Subject: [PATCH 7/7] feat(action): in the GitHub action, use simple setup-python pip caching instead of actions/cache --- action.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/action.yml b/action.yml index b7e43e56e..be0b3750e 100644 --- a/action.yml +++ b/action.yml @@ -53,18 +53,7 @@ runs: uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Get pip cache dir - id: pip-cache - run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - shell: bash - - name: Cache Darker environment - uses: actions/cache@v4 - with: - path: | - ${{ github.action_path }}/.darker-env - ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-darker-${{ inputs.version }}-lint-${{ inputs.lint }} - save-always: true + cache: 'pip' - name: Install dependencies run: | pip install pip-requirements-parser