From 6088ea2c854e351288cc57a23e1dd1b25e77695a Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 4 Feb 2026 09:23:27 +0000 Subject: [PATCH] Fix the PATH mangling in action.yml (#2723) * Fix the PATH mangling in action.yml Let's put the `uv` on PATH question aside, and fix #2663. This change ensures that the PATH entries added by Github when you request `shell: bash` don't make it into the pwsh environment. It also keeps the uv binary on PATH if installed via `extras:`. Would love to get this fixed soon, then we can figure out the right approach regarding `uv` discovery. * Modify script to allow testing of the GHA action on a PR Cherry-picked from #2718 and modified so we can test the action.yml file --- action.yml | 46 +++++++++++++++++++---------------- bin/run_example_ci_configs.py | 41 +++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/action.yml b/action.yml index 48706d1d..3e7062cc 100644 --- a/action.yml +++ b/action.yml @@ -69,49 +69,53 @@ runs: venv_path = Path(r"${{ runner.temp }}") / "cibw" if venv_path.exists(): shutil.rmtree(venv_path) + builder = EnvBuilder() builder.create(venv_path) exposed_binaries = {"cibuildwheel"} + if "uv" in EXTRAS: exposed_binaries.add("uv") clean_bin_path = builder.bin_path.parent / f"{builder.bin_path.name}.clean" clean_bin_path.mkdir() + for path in list(builder.bin_path.iterdir()): if path.stem in exposed_binaries: try: os.symlink(path, clean_bin_path / path.name) except OSError: - import shutil - shutil.copy2(path, clean_bin_path / path.name) - full_path = f"{clean_bin_path}{os.pathsep}{os.environ['PATH']}" + + cibw_bin = [p for p in builder.bin_path.glob("cibuildwheel*") if p.stem == "cibuildwheel"][0] + with open(os.environ["GITHUB_OUTPUT"], "at") as f: - f.write(f"updated-path={full_path}\n") + f.write(f"cibw-bin={cibw_bin}\n") + f.write(f"prepend-path={clean_bin_path}\n") + print("::endgroup::") EOF shell: bash # Redirecting stderr to stdout to fix interleaving issue in Actions. - - run: > - cibuildwheel - "${{ inputs.package-dir }}" - ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} - ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} - ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} - 2>&1 - env: - PATH: "${{ steps.cibw.outputs.updated-path }}" + - run: | + export PATH="${{ steps.cibw.outputs.prepend-path }}:$PATH" + + "${{ steps.cibw.outputs.cibw-bin }}" \ + "${{ inputs.package-dir }}" \ + ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} \ + ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} \ + ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} \ + 2>&1 shell: bash if: runner.os != 'Windows' # Windows needs powershell to interact nicely with Meson - - run: > - cibuildwheel - "${{ inputs.package-dir }}" - ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} - ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} - ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} - env: - PATH: "${{ steps.cibw.outputs.updated-path }}" + - run: | + $env:PATH = "${{ steps.cibw.outputs.prepend-path }};$env:PATH" + & "${{ steps.cibw.outputs.cibw-bin }}" ` + "${{ inputs.package-dir }}" ` + ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} ` + ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} ` + ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} shell: pwsh if: runner.os == 'Windows' diff --git a/bin/run_example_ci_configs.py b/bin/run_example_ci_configs.py index da8d5b6a..c01e2fad 100755 --- a/bin/run_example_ci_configs.py +++ b/bin/run_example_ci_configs.py @@ -39,10 +39,10 @@ class CIService(typing.NamedTuple): name: str dst_config_path: str badge_md: str - config_file_transform: typing.Callable[[str], str] = lambda x: x # identity by default + config_file_transform: typing.Callable[[str, str], str] = lambda x, _: x # identity by default -def github_config_file_transform(content: str) -> str: +def github_config_file_transform(content: str, git_ref: str) -> str: # one of the the github configs only builds on main, so we need to remove that restriction # so our example build will run on the test branch. # @@ -61,6 +61,22 @@ def github_config_file_transform(content: str) -> str: "push:", content, ) + + # use the version of cibuildwheel from the current commit, not the latest + # release + # replace: + # """ + # uses: pypa/cibuildwheel@v3.3.1 + # """ + # with: + # """ + # uses: pypa/cibuildwheel@ + # """ + content = re.sub( + r"uses: pypa/cibuildwheel@v.*", + f"uses: pypa/cibuildwheel@{git_ref}", + content, + ) return content @@ -110,20 +126,22 @@ def ci_service_for_config_file(config_file: Path) -> CIService: @click.command() @click.argument("config_files", nargs=-1, type=click.Path()) -def run_example_ci_configs(config_files=None): +def run_example_ci_configs( + config_files: list[str], +) -> None: """ Test the example configs. If no files are specified, will test examples/*-minimal.yml """ if len(config_files) == 0: - config_files = Path("examples").glob("*-minimal.yml") + config_file_paths = list(Path("examples").glob("*-minimal.yml")) else: - config_files = [Path(f) for f in config_files] + config_file_paths = [Path(f) for f in config_files] # check each CI service has at most 1 config file configs_by_service = set() - for config_file in config_files: + for config_file in config_file_paths: service = ci_service_for_config_file(config_file) if service.name in configs_by_service: msg = "You cannot specify more than one config per CI service" @@ -137,6 +155,9 @@ def run_example_ci_configs(config_files=None): previous_branch = shell( "git rev-parse --abbrev-ref HEAD", check=True, capture_output=True, encoding="utf8" ).stdout.strip() + git_ref = shell( + "git rev-parse HEAD", check=True, capture_output=True, encoding="utf8" + ).stdout.strip() timestamp = time.strftime("%Y-%m-%dT%H-%M-%S", time.gmtime()) branch_name = f"example-config-test---{previous_branch}-{timestamp}" @@ -147,14 +168,14 @@ def run_example_ci_configs(config_files=None): example_project = Path("example_root") generate_basic_project(example_project) - for config_file in config_files: + for config_file in config_file_paths: service = ci_service_for_config_file(config_file) dst_config_file = example_project / service.dst_config_path dst_config_file.parent.mkdir(parents=True, exist_ok=True) contents = config_file.read_text(encoding="utf8") - contents = service.config_file_transform(contents) + contents = service.config_file_transform(contents, git_ref) dst_config_file.write_text(contents, encoding="utf8") subprocess.run(["git", "add", example_project], check=True) @@ -178,14 +199,14 @@ def run_example_ci_configs(config_files=None): print("> ") print("> | Service | Config | Status |") print("> |---|---|---|") - for config_file in config_files: + for config_file in config_file_paths: service = ci_service_for_config_file(config_file) badge = service.badge_md.format( branch=branch_name, branch_escaped=quote(branch_name, safe="") ) print(f"> | {service.name} | `{config_file}` | {badge} |") print("> ") - print("> Generated by `bin/run_example_ci_config.py`") + print(f"> Generated by `{' '.join(sys.argv)}`") print() print("---") finally: