Skip to content

Commit 1c84d31

Browse files
committed
[github-actions] Use remote action for PHP version resolution (#76)
1 parent c1da6e0 commit 1c84d31

9 files changed

Lines changed: 192 additions & 417 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
name: Resolve PHP Version
2+
description: Resolve the PHP version and test matrix from composer.lock or composer.json.
3+
4+
outputs:
5+
php-version:
6+
description: Resolved PHP version used by setup-php.
7+
value: ${{ steps.resolve.outputs.php-version }}
8+
php-version-source:
9+
description: Source used to resolve the PHP version.
10+
value: ${{ steps.resolve.outputs.php-version-source }}
11+
test-matrix:
12+
description: JSON matrix of supported PHP minors starting from the inferred minimum.
13+
value: ${{ steps.resolve.outputs.test-matrix }}
14+
warning:
15+
description: Warning emitted when the workflow falls back to the default PHP version.
16+
value: ${{ steps.resolve.outputs.warning }}
17+
18+
runs:
19+
using: composite
20+
steps:
21+
- name: Resolve workflow PHP version
22+
id: resolve
23+
shell: bash
24+
run: |
25+
python3 <<'PY'
26+
from __future__ import annotations
27+
28+
import json
29+
import os
30+
import re
31+
from pathlib import Path
32+
33+
SUPPORTED_MINORS = ["8.3", "8.4", "8.5"]
34+
DEFAULT_PHP_VERSION = "8.3"
35+
36+
def version_to_tuple(version: str) -> tuple[int, int]:
37+
major, minor = version.split(".")
38+
return int(major), int(minor)
39+
40+
def normalize_minor(version: str) -> str | None:
41+
match = re.match(r"^\s*v?(8)\.(\d+)(?:\.\d+)?(?:\.\*)?\s*$", version)
42+
if match is None:
43+
return None
44+
return f"{match.group(1)}.{match.group(2)}"
45+
46+
def next_supported_minor(version: str) -> str | None:
47+
if version not in SUPPORTED_MINORS:
48+
return None
49+
index = SUPPORTED_MINORS.index(version) + 1
50+
if index >= len(SUPPORTED_MINORS):
51+
major, minor = version_to_tuple(version)
52+
return f"{major}.{minor + 1}"
53+
return SUPPORTED_MINORS[index]
54+
55+
def infer_clause_lower_bound(clause: str) -> str | None:
56+
tokens = re.findall(r"(\^|~|>=|>|<=|<|==|=)?\s*v?(8\.\d+(?:\.\d+)?(?:\.\*)?)", clause)
57+
lower_bounds: list[str] = []
58+
for operator, version in tokens:
59+
normalized = normalize_minor(version)
60+
if normalized is None:
61+
continue
62+
if operator in ("", "=", "==", "^", "~", ">="):
63+
lower_bounds.append(normalized)
64+
continue
65+
if operator == ">":
66+
next_minor = next_supported_minor(normalized)
67+
if next_minor is not None:
68+
lower_bounds.append(next_minor)
69+
if not lower_bounds:
70+
return None
71+
return max(lower_bounds, key=version_to_tuple)
72+
73+
def infer_minimum_supported_minor(requirement: str) -> str | None:
74+
clauses = [clause.strip() for clause in requirement.split("||")]
75+
lower_bounds = [
76+
clause_lower_bound
77+
for clause in clauses
78+
if (clause_lower_bound := infer_clause_lower_bound(clause)) is not None
79+
]
80+
if not lower_bounds:
81+
return None
82+
return min(lower_bounds, key=version_to_tuple)
83+
84+
def resolve_from_lock(composer_lock: Path) -> tuple[str | None, str | None]:
85+
if not composer_lock.exists():
86+
return None, None
87+
try:
88+
payload = json.loads(composer_lock.read_text())
89+
except json.JSONDecodeError:
90+
return None, "composer.lock exists but could not be parsed"
91+
platform_overrides = payload.get("platform-overrides") or {}
92+
platform_php = platform_overrides.get("php")
93+
if isinstance(platform_php, str):
94+
resolved = normalize_minor(platform_php)
95+
if resolved is not None:
96+
return resolved, "composer.lock platform-overrides.php"
97+
return None, "composer.lock platform-overrides.php is not a supported PHP version"
98+
return None, None
99+
100+
def resolve_from_json(composer_json: Path) -> tuple[str | None, str | None]:
101+
if not composer_json.exists():
102+
return None, "composer.json does not exist"
103+
try:
104+
payload = json.loads(composer_json.read_text())
105+
except json.JSONDecodeError:
106+
return None, "composer.json could not be parsed"
107+
config_platform_php = (((payload.get("config") or {}).get("platform") or {}).get("php"))
108+
if isinstance(config_platform_php, str):
109+
resolved = normalize_minor(config_platform_php)
110+
if resolved is not None:
111+
return resolved, "composer.json config.platform.php"
112+
return None, "composer.json config.platform.php is not a supported PHP version"
113+
require_php = ((payload.get("require") or {}).get("php"))
114+
if isinstance(require_php, str):
115+
resolved = infer_minimum_supported_minor(require_php)
116+
if resolved is not None:
117+
return resolved, "composer.json require.php"
118+
return None, "composer.json require.php could not be resolved safely"
119+
return None, None
120+
121+
def resolve_php_version() -> tuple[str, str, str | None]:
122+
resolved, source = resolve_from_lock(Path("composer.lock"))
123+
if resolved is None:
124+
resolved, source = resolve_from_json(Path("composer.json"))
125+
if resolved is None:
126+
return DEFAULT_PHP_VERSION, "fallback", "No reliable PHP version source was found. Falling back to 8.3."
127+
if resolved not in SUPPORTED_MINORS:
128+
return DEFAULT_PHP_VERSION, "fallback", (
129+
f"Resolved PHP version {resolved} from {source} is outside the supported CI policy. Falling back to 8.3."
130+
)
131+
return resolved, source or "fallback", None
132+
133+
resolved_version, source, warning = resolve_php_version()
134+
matrix_versions = [version for version in SUPPORTED_MINORS if version_to_tuple(version) >= version_to_tuple(resolved_version)]
135+
matrix = json.dumps({"php-version": matrix_versions}, separators=(",", ":"))
136+
137+
print(f"Resolved PHP version source: {source}")
138+
print(f"Resolved PHP version: {resolved_version}")
139+
print(f"Resolved PHP test matrix: {matrix_versions}")
140+
if warning:
141+
print(f"Warning: {warning}")
142+
143+
github_output = os.environ["GITHUB_OUTPUT"]
144+
with open(github_output, "a", encoding="utf-8") as handle:
145+
handle.write(f"php-version={resolved_version}\n")
146+
handle.write(f"php-version-source={source}\n")
147+
handle.write(f"test-matrix={matrix}\n")
148+
handle.write(f"warning={warning or ''}\n")
149+
PY

.github/scripts/resolve_php_version.py

Lines changed: 0 additions & 193 deletions
This file was deleted.

.github/workflows/reports.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242

4343
- name: Resolve workflow PHP version
4444
id: resolve
45-
run: python3 .github/scripts/resolve_php_version.py
45+
uses: php-fast-forward/dev-tools/.github/actions/resolve-php-version@main
4646

4747
reports:
4848
needs: resolve_php

.github/workflows/tests.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ on:
4141
- 'tests/**'
4242
- 'composer.json'
4343
- 'composer.lock'
44-
- '.github/scripts/**'
45-
- 'resources/github-actions/scripts/**'
44+
- '.github/actions/**'
4645
- '.github/workflows/tests.yml'
4746
push:
4847
branches: [ "main" ]
@@ -68,7 +67,7 @@ jobs:
6867

6968
- name: Resolve workflow PHP version
7069
id: resolve
71-
run: python3 .github/scripts/resolve_php_version.py
70+
uses: php-fast-forward/dev-tools/.github/actions/resolve-php-version@main
7271

7372
tests:
7473
needs: resolve_php

.github/workflows/wiki.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030

3131
- name: Resolve workflow PHP version
3232
id: resolve
33-
run: python3 .github/scripts/resolve_php_version.py
33+
uses: php-fast-forward/dev-tools/.github/actions/resolve-php-version@main
3434

3535
preview:
3636
needs: resolve_php

0 commit comments

Comments
 (0)