Skip to content
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
25 changes: 25 additions & 0 deletions actions/setup-release-tool/action.yml
Original file line number Diff line number Diff line change
@@ -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"
52 changes: 52 additions & 0 deletions actions/setup-release-tool/setup.sh
Original file line number Diff line number Diff line change
@@ -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."
131 changes: 131 additions & 0 deletions tests/test_setup_release_tool_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# 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

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,
*,
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"
path_file = root / "path"
env = os.environ | {
"RELEASE_TOOL_VERSION": version,
"RUNNER_TEMP": str(root),
"GITHUB_OUTPUT": str(output),
"GITHUB_PATH": str(path_file),
}

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")

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_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)


if __name__ == "__main__":
unittest.main()
Loading