Skip to content

Commit

Permalink
Merge pull request #591 from akaihola/gh-action-working-directory
Browse files Browse the repository at this point in the history
GH action to respect `working-directory`
  • Loading branch information
akaihola authored Jan 7, 2025
2 parents 25d1f1b + 33eb979 commit 767e00e
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 23 deletions.
107 changes: 107 additions & 0 deletions .github/workflows/test-working-directory.yml
Original file line number Diff line number Diff line change
@@ -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 "[email protected]"
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
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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
Expand Down
26 changes: 25 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -35,8 +48,18 @@ runs:
steps:
- name: Commit Range
id: commit-range
uses: akaihola/darker/.github/actions/[email protected]
uses: ./.github/actions/commit-range
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
cache: 'pip'
- 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
Expand Down Expand Up @@ -68,5 +91,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
63 changes: 49 additions & 14 deletions action/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,68 @@
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(
"::notice:: Baseline linting has been moved to the Graylint package."
" 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}<<DARKER_ACTION_EOF", file=f)
print(val, file=f, end="" if val.endswith("\n") else "\n")
print("DARKER_ACTION_EOF", file=f)
else:
print(f"{key}={val}", file=f)


def exit_with_exitcode(exitcode: int) -> 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}"
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")]
Expand All @@ -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)
44 changes: 36 additions & 8 deletions action/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):

Expand All @@ -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(
Expand All @@ -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):

Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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<<DARKER_ACTION_EOF"],
)
def test_writes_github_output(main_patch, github_output, expect_line):
"""A successful run outputs a zero exit code and output from Darker."""
with pytest.raises(SysExitCalled):

run_module("main")

assert expect_line in github_output.read_text().splitlines()

0 comments on commit 767e00e

Please sign in to comment.