Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH action to respect working-directory #591

Merged
merged 7 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
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()
Loading