Skip to content

Commit cb1515e

Browse files
authored
feat: add verified release-tool setup action
Add the reusable action for installing exact release-tool PHAR versions with mandatory SHA-256 verification and failure tests. Signed-off-by: Vitor Mattos <vitor@php.rio>
1 parent deb5fdb commit cb1515e

3 files changed

Lines changed: 208 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
name: Setup release-tool
5+
description: Install an exact LibreCode release-tool PHAR release and verify its published SHA-256.
6+
inputs:
7+
version:
8+
description: Exact release-tool semantic version, without a leading v.
9+
required: true
10+
outputs:
11+
path:
12+
description: Absolute path to the verified release-tool PHAR.
13+
value: ${{ steps.install.outputs.path }}
14+
version:
15+
description: Installed release-tool version.
16+
value: ${{ steps.install.outputs.version }}
17+
runs:
18+
using: composite
19+
steps:
20+
- id: install
21+
name: Download and verify release-tool
22+
shell: bash
23+
env:
24+
RELEASE_TOOL_VERSION: ${{ inputs.version }}
25+
run: bash "${{ github.action_path }}/setup.sh"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
set -euo pipefail
6+
7+
version="${RELEASE_TOOL_VERSION:?release-tool version is required}"
8+
9+
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
10+
echo "::error::release-tool version must be an exact semantic version, got '${version}'"
11+
exit 2
12+
fi
13+
14+
case "${version}" in
15+
latest|stable|main|master|*'*'*|*'^'*|*'~'*|*'>'*|*'<'*)
16+
echo "::error::floating release-tool versions are forbidden"
17+
exit 2
18+
;;
19+
esac
20+
21+
install_root="${RUNNER_TEMP:-/tmp}/librecode-release-tool/${version}"
22+
mkdir -p "${install_root}"
23+
24+
base_url="https://github.com/LibreCodeCoop/release-tool/releases/download/v${version}"
25+
phar="${install_root}/release-tool.phar"
26+
checksum="${install_root}/release-tool.phar.sha256"
27+
28+
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 "${base_url}/release-tool.phar" --output "${phar}"
29+
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 "${base_url}/release-tool.phar.sha256" --output "${checksum}"
30+
31+
expected="$(awk 'NR == 1 {print $1}' "${checksum}")"
32+
if [[ ! "${expected}" =~ ^[0-9a-fA-F]{64}$ ]]; then
33+
echo "::error::published release-tool checksum is malformed"
34+
exit 3
35+
fi
36+
37+
actual="$(sha256sum "${phar}" | awk '{print $1}')"
38+
if [[ "${actual,,}" != "${expected,,}" ]]; then
39+
echo "::error::release-tool checksum mismatch"
40+
exit 3
41+
fi
42+
43+
chmod 0755 "${phar}"
44+
php "${phar}" --version >/dev/null
45+
46+
{
47+
echo "path=${phar}"
48+
echo "version=${version}"
49+
} >> "${GITHUB_OUTPUT}"
50+
51+
echo "${install_root}" >> "${GITHUB_PATH}"
52+
echo "Installed release-tool ${version} with verified SHA-256."
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
import os
5+
import subprocess
6+
import tempfile
7+
import unittest
8+
from pathlib import Path
9+
10+
ROOT = Path(__file__).resolve().parents[1]
11+
SCRIPT = ROOT / "actions" / "setup-release-tool" / "setup.sh"
12+
ACTION = ROOT / "actions" / "setup-release-tool" / "action.yml"
13+
14+
15+
class SetupReleaseToolActionTest(unittest.TestCase):
16+
def run_script(
17+
self,
18+
version: str,
19+
*,
20+
fake_download: bool = False,
21+
checksum_mismatch: bool = False,
22+
) -> tuple[subprocess.CompletedProcess[str], str, str]:
23+
with tempfile.TemporaryDirectory() as directory:
24+
root = Path(directory)
25+
output = root / "output"
26+
path_file = root / "path"
27+
env = os.environ | {
28+
"RELEASE_TOOL_VERSION": version,
29+
"RUNNER_TEMP": str(root),
30+
"GITHUB_OUTPUT": str(output),
31+
"GITHUB_PATH": str(path_file),
32+
}
33+
34+
if fake_download:
35+
fake_bin = root / "bin"
36+
fake_bin.mkdir()
37+
curl = fake_bin / "curl"
38+
curl.write_text(
39+
"#!/usr/bin/env bash\n"
40+
"set -euo pipefail\n"
41+
"output=''\n"
42+
"url=''\n"
43+
"while (($#)); do\n"
44+
" case \"$1\" in\n"
45+
" --output) output=\"$2\"; shift 2 ;;\n"
46+
" http*) url=\"$1\"; shift ;;\n"
47+
" *) shift ;;\n"
48+
" esac\n"
49+
"done\n"
50+
"if [[ \"$url\" == *.sha256 ]]; then\n"
51+
" if [[ \"${FAKE_CHECKSUM_MISMATCH:-0}\" == 1 ]]; then\n"
52+
" printf '%064d release-tool.phar\\n' 0 > \"$output\"\n"
53+
" else\n"
54+
" printf fake-phar | sha256sum | awk '{print $1 \" release-tool.phar\"}' > \"$output\"\n"
55+
" fi\n"
56+
"else\n"
57+
" printf fake-phar > \"$output\"\n"
58+
"fi\n",
59+
encoding="utf-8",
60+
)
61+
curl.chmod(0o755)
62+
63+
php = fake_bin / "php"
64+
php.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
65+
php.chmod(0o755)
66+
67+
env["PATH"] = str(fake_bin) + os.pathsep + env["PATH"]
68+
if checksum_mismatch:
69+
env["FAKE_CHECKSUM_MISMATCH"] = "1"
70+
71+
result = subprocess.run(
72+
["bash", str(SCRIPT)],
73+
env=env,
74+
text=True,
75+
capture_output=True,
76+
check=False,
77+
)
78+
return (
79+
result,
80+
output.read_text(encoding="utf-8") if output.exists() else "",
81+
path_file.read_text(encoding="utf-8") if path_file.exists() else "",
82+
)
83+
84+
def test_action_exposes_only_exact_version_input(self) -> None:
85+
content = ACTION.read_text(encoding="utf-8")
86+
87+
self.assertIn("version:", content)
88+
self.assertNotIn("latest", content.lower())
89+
self.assertIn("setup.sh", content)
90+
91+
def test_rejects_latest_before_network_access(self) -> None:
92+
result, _, _ = self.run_script("latest")
93+
94+
self.assertEqual(2, result.returncode)
95+
self.assertIn("exact semantic version", result.stdout)
96+
97+
def test_rejects_semver_range_before_network_access(self) -> None:
98+
result, _, _ = self.run_script("^1.2.3")
99+
100+
self.assertEqual(2, result.returncode)
101+
self.assertIn("exact semantic version", result.stdout)
102+
103+
def test_valid_exact_release_is_verified_and_exposed(self) -> None:
104+
result, output, path_file = self.run_script("1.2.3", fake_download=True)
105+
106+
self.assertEqual(0, result.returncode, result.stderr)
107+
self.assertIn("version=1.2.3", output)
108+
self.assertIn("path=", output)
109+
self.assertIn("librecode-release-tool/1.2.3", path_file)
110+
111+
def test_checksum_mismatch_fails_closed(self) -> None:
112+
result, output, _ = self.run_script(
113+
"1.2.3",
114+
fake_download=True,
115+
checksum_mismatch=True,
116+
)
117+
118+
self.assertEqual(3, result.returncode)
119+
self.assertIn("checksum mismatch", result.stdout)
120+
self.assertEqual("", output)
121+
122+
def test_script_downloads_phar_and_published_checksum_from_same_exact_tag(self) -> None:
123+
content = SCRIPT.read_text(encoding="utf-8")
124+
125+
self.assertIn("releases/download/v${version}", content)
126+
self.assertIn("release-tool.phar.sha256", content)
127+
self.assertIn("sha256sum", content)
128+
129+
130+
if __name__ == "__main__":
131+
unittest.main()

0 commit comments

Comments
 (0)