From 6463450514796178b4d595da6171914581f9154d Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 02:39:01 -0300 Subject: [PATCH 1/4] feat: add pinned release-tool setup action Signed-off-by: Vitor Mattos --- actions/setup-release-tool/action.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 actions/setup-release-tool/action.yml diff --git a/actions/setup-release-tool/action.yml b/actions/setup-release-tool/action.yml new file mode 100644 index 0000000..f647ce1 --- /dev/null +++ b/actions/setup-release-tool/action.yml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Setup release-tool +description: Install an exact LibreCode release-tool PHAR release and verify its published SHA-256. +inputs: + version: + description: Exact release-tool semantic version, without a leading v. + required: true +outputs: + path: + description: Absolute path to the verified release-tool PHAR. + value: ${{ steps.install.outputs.path }} + version: + description: Installed release-tool version. + value: ${{ steps.install.outputs.version }} +runs: + using: composite + steps: + - id: install + name: Download and verify release-tool + shell: bash + env: + RELEASE_TOOL_VERSION: ${{ inputs.version }} + run: bash "${{ github.action_path }}/setup.sh" From 388ab84001718299533815375400b6449f003767 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 02:39:03 -0300 Subject: [PATCH 2/4] feat: add pinned release-tool setup action Signed-off-by: Vitor Mattos --- actions/setup-release-tool/setup.sh | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 actions/setup-release-tool/setup.sh diff --git a/actions/setup-release-tool/setup.sh b/actions/setup-release-tool/setup.sh new file mode 100644 index 0000000..b48c2f5 --- /dev/null +++ b/actions/setup-release-tool/setup.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -euo pipefail + +version="${RELEASE_TOOL_VERSION:?release-tool version is required}" + +if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::release-tool version must be an exact semantic version, got '${version}'" + exit 2 +fi + +case "${version}" in + latest|stable|main|master|*'*'*|*'^'*|*'~'*|*'>'*|*'<'*) + echo "::error::floating release-tool versions are forbidden" + exit 2 + ;; +esac + +install_root="${RUNNER_TEMP:-/tmp}/librecode-release-tool/${version}" +mkdir -p "${install_root}" + +base_url="https://github.com/LibreCodeCoop/release-tool/releases/download/v${version}" +phar="${install_root}/release-tool.phar" +checksum="${install_root}/release-tool.phar.sha256" + +curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 "${base_url}/release-tool.phar" --output "${phar}" +curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 "${base_url}/release-tool.phar.sha256" --output "${checksum}" + +expected="$(awk 'NR == 1 {print $1}' "${checksum}")" +if [[ ! "${expected}" =~ ^[0-9a-fA-F]{64}$ ]]; then + echo "::error::published release-tool checksum is malformed" + exit 3 +fi + +actual="$(sha256sum "${phar}" | awk '{print $1}')" +if [[ "${actual,,}" != "${expected,,}" ]]; then + echo "::error::release-tool checksum mismatch" + exit 3 +fi + +chmod 0755 "${phar}" +php "${phar}" --version >/dev/null + +{ + echo "path=${phar}" + echo "version=${version}" +} >> "${GITHUB_OUTPUT}" + +echo "${install_root}" >> "${GITHUB_PATH}" +echo "Installed release-tool ${version} with verified SHA-256." From 51148dd7054db8a6653b5f2dc1a00a06a4e4b292 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 02:39:06 -0300 Subject: [PATCH 3/4] feat: add pinned release-tool setup action Signed-off-by: Vitor Mattos --- tests/test_setup_release_tool_action.py | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_setup_release_tool_action.py diff --git a/tests/test_setup_release_tool_action.py b/tests/test_setup_release_tool_action.py new file mode 100644 index 0000000..a69e1ae --- /dev/null +++ b/tests/test_setup_release_tool_action.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "actions" / "setup-release-tool" / "setup.sh" +ACTION = ROOT / "actions" / "setup-release-tool" / "action.yml" + + +class SetupReleaseToolActionTest(unittest.TestCase): + def run_script(self, version: str) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + output = root / "output" + path_file = root / "path" + env = os.environ | { + "RELEASE_TOOL_VERSION": version, + "RUNNER_TEMP": str(root), + "GITHUB_OUTPUT": str(output), + "GITHUB_PATH": str(path_file), + } + return subprocess.run( + ["bash", str(SCRIPT)], + env=env, + text=True, + capture_output=True, + check=False, + ) + + def test_action_exposes_only_exact_version_input(self) -> None: + content = ACTION.read_text(encoding="utf-8") + + self.assertIn("version:", content) + self.assertNotIn("latest", content.lower()) + self.assertIn("setup.sh", content) + + def test_rejects_latest_before_network_access(self) -> None: + result = self.run_script("latest") + + self.assertEqual(2, result.returncode) + self.assertIn("exact semantic version", result.stdout) + + def test_rejects_semver_range_before_network_access(self) -> None: + result = self.run_script("^1.2.3") + + self.assertEqual(2, result.returncode) + self.assertIn("exact semantic version", result.stdout) + + def test_script_downloads_phar_and_published_checksum_from_same_exact_tag(self) -> None: + content = SCRIPT.read_text(encoding="utf-8") + + self.assertIn("releases/download/v${version}", content) + self.assertIn("release-tool.phar.sha256", content) + self.assertIn("sha256sum", content) + self.assertIn("checksum mismatch", content) + + +if __name__ == "__main__": + unittest.main() From bc2dd54b64746e83fcb14e5a372cf7d4519d35a7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 02:39:51 -0300 Subject: [PATCH 4/4] test: exercise release-tool checksum verification Signed-off-by: Vitor Mattos --- tests/test_setup_release_tool_action.py | 78 +++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/tests/test_setup_release_tool_action.py b/tests/test_setup_release_tool_action.py index a69e1ae..dd0e6c5 100644 --- a/tests/test_setup_release_tool_action.py +++ b/tests/test_setup_release_tool_action.py @@ -6,7 +6,6 @@ import tempfile import unittest from pathlib import Path -from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "actions" / "setup-release-tool" / "setup.sh" @@ -14,7 +13,13 @@ class SetupReleaseToolActionTest(unittest.TestCase): - def run_script(self, version: str) -> subprocess.CompletedProcess[str]: + def run_script( + self, + version: str, + *, + fake_download: bool = False, + checksum_mismatch: bool = False, + ) -> tuple[subprocess.CompletedProcess[str], str, str]: with tempfile.TemporaryDirectory() as directory: root = Path(directory) output = root / "output" @@ -25,13 +30,56 @@ def run_script(self, version: str) -> subprocess.CompletedProcess[str]: "GITHUB_OUTPUT": str(output), "GITHUB_PATH": str(path_file), } - return subprocess.run( + + if fake_download: + fake_bin = root / "bin" + fake_bin.mkdir() + curl = fake_bin / "curl" + curl.write_text( + "#!/usr/bin/env bash\n" + "set -euo pipefail\n" + "output=''\n" + "url=''\n" + "while (($#)); do\n" + " case \"$1\" in\n" + " --output) output=\"$2\"; shift 2 ;;\n" + " http*) url=\"$1\"; shift ;;\n" + " *) shift ;;\n" + " esac\n" + "done\n" + "if [[ \"$url\" == *.sha256 ]]; then\n" + " if [[ \"${FAKE_CHECKSUM_MISMATCH:-0}\" == 1 ]]; then\n" + " printf '%064d release-tool.phar\\n' 0 > \"$output\"\n" + " else\n" + " printf fake-phar | sha256sum | awk '{print $1 \" release-tool.phar\"}' > \"$output\"\n" + " fi\n" + "else\n" + " printf fake-phar > \"$output\"\n" + "fi\n", + encoding="utf-8", + ) + curl.chmod(0o755) + + php = fake_bin / "php" + php.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8") + php.chmod(0o755) + + env["PATH"] = str(fake_bin) + os.pathsep + env["PATH"] + if checksum_mismatch: + env["FAKE_CHECKSUM_MISMATCH"] = "1" + + result = subprocess.run( ["bash", str(SCRIPT)], env=env, text=True, capture_output=True, check=False, ) + return ( + result, + output.read_text(encoding="utf-8") if output.exists() else "", + path_file.read_text(encoding="utf-8") if path_file.exists() else "", + ) def test_action_exposes_only_exact_version_input(self) -> None: content = ACTION.read_text(encoding="utf-8") @@ -41,24 +89,42 @@ def test_action_exposes_only_exact_version_input(self) -> None: self.assertIn("setup.sh", content) def test_rejects_latest_before_network_access(self) -> None: - result = self.run_script("latest") + result, _, _ = self.run_script("latest") self.assertEqual(2, result.returncode) self.assertIn("exact semantic version", result.stdout) def test_rejects_semver_range_before_network_access(self) -> None: - result = self.run_script("^1.2.3") + result, _, _ = self.run_script("^1.2.3") self.assertEqual(2, result.returncode) self.assertIn("exact semantic version", result.stdout) + def test_valid_exact_release_is_verified_and_exposed(self) -> None: + result, output, path_file = self.run_script("1.2.3", fake_download=True) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("version=1.2.3", output) + self.assertIn("path=", output) + self.assertIn("librecode-release-tool/1.2.3", path_file) + + def test_checksum_mismatch_fails_closed(self) -> None: + result, output, _ = self.run_script( + "1.2.3", + fake_download=True, + checksum_mismatch=True, + ) + + self.assertEqual(3, result.returncode) + self.assertIn("checksum mismatch", result.stdout) + self.assertEqual("", output) + def test_script_downloads_phar_and_published_checksum_from_same_exact_tag(self) -> None: content = SCRIPT.read_text(encoding="utf-8") self.assertIn("releases/download/v${version}", content) self.assertIn("release-tool.phar.sha256", content) self.assertIn("sha256sum", content) - self.assertIn("checksum mismatch", content) if __name__ == "__main__":