Skip to content

Commit 727a6e6

Browse files
committed
Fix
1 parent db4e81c commit 727a6e6

2 files changed

Lines changed: 114 additions & 24 deletions

File tree

‎.github/scripts/test-automation.py‎

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
#!/usr/bin/env python3
2-
"""
3-
Standard-library regression fixtures, executed by GitHub's package-check job.
2+
"""Standard-library regression fixtures, executed by GitHub's package-check job.
43
54
These fixtures do not bootstrap WordPress or replace workflow-generated PHP tests.
65
All mutation is confined to a temporary fixture repository on the runner.
76
Processes use system tools in /usr/bin on the Ubuntu runner/WSL target; PATH
8-
overrides select only the intentional curl/grep substitutes inside shell steps.
7+
overrides select only intentional command substitutes inside extracted shell steps.
98
"""
109

1110
from __future__ import annotations
@@ -47,6 +46,16 @@ def workflow_step(filename: str, name: str) -> str:
4746
return textwrap.dedent(section.split(" run: |\n", 1)[1])
4847

4948

49+
def successful_quality_jobs() -> list[dict[str, str]]:
50+
"""Build completed job fixtures from the compatibility workflow's names."""
51+
workflow = (WORKFLOWS / "wp-compatibility-test.yml").read_text()
52+
names = [name for name in re.findall(r"^ name: (.+)$", workflow, re.M) if "${{" not in name]
53+
names += [f"Test WordPress {wp} with PHP {php} (highest deps)"
54+
for php in ("8.2", "8.3", "8.4", "8.5") for wp in ("6.8", "latest", "nightly")]
55+
names.append("Test WordPress latest with PHP 8.2 (lowest deps)")
56+
return [{"name": name, "status": "completed", "conclusion": "success"} for name in names]
57+
58+
5059
class AutomationTests(unittest.TestCase):
5160
def setUp(self):
5261
self.temporary = tempfile.TemporaryDirectory(prefix="es-optimizer-automation-")
@@ -160,18 +169,16 @@ def accepts_run(runs):
160169
(self.root / "bin/jq").chmod(0o700)
161170
with patch.dict(os.environ, PATH=f"{self.root / 'bin'}:{os.environ['PATH']}"):
162171
self.assertTrue(accepts_run([run]))
172+
self.assertTrue(accepts_run([dict(run, event="workflow_dispatch")]))
163173
self.assertFalse(accepts_run([]))
164174
for changes in ({"head_sha": "other"}, {"event": "pull_request"},
165175
{"head_branch": "other"}, {"head_repository": {"full_name": "fork/repo"}},
166-
{"conclusion": "failure"}, {"conclusion": "cancelled"}, {"status": "in_progress"}):
176+
{"conclusion": "failure"}, {"conclusion": "cancelled"},
177+
{"status": "queued"}, {"status": "in_progress"}):
167178
self.assertFalse(accepts_run([dict(run, **changes)]))
168179
self.assertFalse(accepts_run([run, dict(run, id=2, conclusion="failure")]))
169-
workflow = (WORKFLOWS / "wp-compatibility-test.yml").read_text()
170-
names = [name for name in re.findall(r"^ name: (.+)$", workflow, re.M) if "${{" not in name]
171-
names += [f"Test WordPress {wp} with PHP {php} (highest deps)"
172-
for php in ("8.2", "8.3", "8.4", "8.5") for wp in ("6.8", "latest", "nightly")]
173-
names.append("Test WordPress latest with PHP 8.2 (lowest deps)")
174-
jobs = [{"name": name, "status": "completed", "conclusion": "success"} for name in names]
180+
self.assertFalse(accepts_run([run, dict(run, id=2, status="in_progress", conclusion=None)]))
181+
jobs = successful_quality_jobs()
175182

176183
def accepts_jobs(records):
177184
# Repository-owned jq program; job records remain separate JSON data.
@@ -191,6 +198,66 @@ def accepts_jobs(records):
191198
self.assertFalse(accepts_jobs(changed))
192199
self.assertFalse(accepts_jobs(jobs + [jobs[0]]))
193200

201+
def test_release_gate_uses_tested_commit_and_explains_rejections(self):
202+
script = workflow_step("release.yml", "Require successful quality checks for this commit")
203+
self.write("bin/gh", '#!/bin/sh\nprintf "%s\\n" "$@" >> "$FIXTURE_GH_ARGS"\n'
204+
'case "$*" in\n'
205+
' *actions/workflows/wp-compatibility-test.yml/runs*) cat "$FIXTURE_RUNS" ;;\n'
206+
' *actions/runs/1/jobs*) cat "$FIXTURE_JOBS" ;;\n'
207+
' *) exit 92 ;;\nesac\n')
208+
self.write("bin/git", '#!/bin/sh\n[ "$1" = rev-parse ] || exit 92\n'
209+
'[ -n "$FIXTURE_TAG_SHA" ] || exit 1\nprintf "%s\\n" "$FIXTURE_TAG_SHA"\n')
210+
for command in ("gh", "git"):
211+
(self.root / "bin" / command).chmod(0o700)
212+
run = {"id": 1, "head_sha": "tested-commit", "head_branch": "main",
213+
"head_repository": {"full_name": "fixture/repo"},
214+
"event": "push", "status": "completed", "conclusion": "success"}
215+
jobs = successful_quality_jobs()
216+
cases = (
217+
("ready", [run], jobs, "", True),
218+
("missing", [], jobs, "", False),
219+
("queued", [dict(run, status="queued", conclusion=None)], jobs, "", False),
220+
("running", [dict(run, status="in_progress", conclusion=None)], jobs, "", False),
221+
("failed", [dict(run, conclusion="failure")], jobs, "", False),
222+
("missing-job", [run], jobs[:-1], "", False),
223+
("matching-tag", [run], jobs, "tested-commit", True),
224+
("wrong-tag", [run], jobs, "untested-tip", False),
225+
)
226+
for label, runs, records, tag_sha, accepted in cases:
227+
with self.subTest(case=label):
228+
self.write("fixture-runs.json", json.dumps({"workflow_runs": runs}))
229+
self.write("fixture-jobs.json", json.dumps([{"jobs": records}]))
230+
self.write("gh-args", "")
231+
self.write("step-summary", "")
232+
env = dict(os.environ, PATH=f"{self.root / 'bin'}:{os.environ['PATH']}",
233+
GITHUB_REPOSITORY="fixture/repo", VERSION="1.2.3",
234+
GITHUB_SHA="untested-tip", GITHUB_REF_NAME="default-branch",
235+
RELEASE_SHA="tested-commit", RELEASE_BRANCH="main",
236+
GITHUB_SERVER_URL="https://github.invalid", RUNNER_TEMP=str(self.root),
237+
GITHUB_STEP_SUMMARY=str(self.root / "step-summary"),
238+
FIXTURE_RUNS=str(self.root / "fixture-runs.json"),
239+
FIXTURE_JOBS=str(self.root / "fixture-jobs.json"),
240+
FIXTURE_GH_ARGS=str(self.root / "gh-args"), FIXTURE_TAG_SHA=tag_sha)
241+
# Local command substitutes need no inherited GitHub credentials.
242+
env.pop("GH_TOKEN", None)
243+
env.pop("GITHUB_TOKEN", None)
244+
# Fixed repository-owned shell step; gh/git are local fixture substitutes.
245+
result = subprocess.run( # nosec B603
246+
["/usr/bin/bash", "-e", "-o", "pipefail", "-c", script],
247+
env=env, capture_output=True, text=True
248+
)
249+
self.assertEqual(accepted, result.returncode == 0, result.stdout + result.stderr)
250+
arguments = (self.root / "gh-args").read_text()
251+
self.assertIn("head_sha=tested-commit\n", arguments)
252+
self.assertIn("branch=main\n", arguments)
253+
self.assertNotIn("untested-tip", arguments)
254+
summary = (self.root / "step-summary").read_text()
255+
if accepted:
256+
self.assertIn("https://github.invalid/fixture/repo/actions/runs/1", summary)
257+
else:
258+
self.assertIn("::error::", result.stdout)
259+
self.assertEqual("", summary)
260+
194261
def test_release_lookup_fails_closed_on_http_and_transport_errors(self):
195262
script = workflow_step("release.yml", "Check if release exists")
196263
self.write("bin/curl", '#!/bin/sh\nprintf "%s" "$FIXTURE_HTTP_STATUS"\nexit "$FIXTURE_CURL_EXIT"\n')

‎.github/workflows/release.yml‎

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
name: Create Release
22

33
on:
4-
push:
5-
# Trigger on pushes to the main or master branch.
4+
# Start after quality checks finish, rather than racing them on the same push.
5+
workflow_run:
6+
workflows: [ "WordPress Compatibility & Plugin Check" ]
7+
types: [ completed ]
68
branches: [ main, master ]
79
# Allow manual triggering.
810
workflow_dispatch:
@@ -12,21 +14,33 @@ permissions:
1214
actions: read
1315

1416
concurrency:
15-
group: release-${{ github.ref }}
17+
group: release-${{ github.event.workflow_run.head_branch || github.ref_name }}
1618
cancel-in-progress: false
1719

1820
env:
1921
PLUGIN_SLUG: enginescript-site-optimizer
2022

2123
jobs:
2224
check-and-release:
23-
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
25+
if: >-
26+
(github.event_name == 'workflow_dispatch' &&
27+
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) ||
28+
(github.event_name == 'workflow_run' &&
29+
github.event.workflow_run.conclusion == 'success' &&
30+
github.event.workflow_run.head_repository.full_name == github.repository &&
31+
(github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_dispatch') &&
32+
(github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master'))
2433
runs-on: ubuntu-latest
2534
timeout-minutes: 15
35+
env:
36+
# workflow_run's github.sha is the default-branch tip, not necessarily the tested commit.
37+
RELEASE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
38+
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
2639
steps:
2740
- name: Checkout code
2841
uses: actions/checkout@v7
2942
with:
43+
ref: ${{ env.RELEASE_SHA }}
3044
fetch-depth: 0
3145

3246
- name: Get latest version
@@ -63,25 +77,28 @@ jobs:
6377
GH_TOKEN: ${{ github.token }}
6478
VERSION: ${{ steps.get_version.outputs.version }}
6579
run: |
66-
# Fail closed while checks are queued/running. Rerun this release job
67-
# after the exact commit's compatibility workflow has passed.
80+
# Automatic releases start after quality succeeds; manual runs must also
81+
# prove every gate for the exact commit checked out above.
6882
gh api --method GET \
6983
"repos/$GITHUB_REPOSITORY/actions/workflows/wp-compatibility-test.yml/runs" \
70-
-f head_sha="$GITHUB_SHA" -f branch="$GITHUB_REF_NAME" -f per_page=100 \
84+
-f head_sha="$RELEASE_SHA" -f branch="$RELEASE_BRANCH" -f per_page=100 \
7185
> "$RUNNER_TEMP/quality-runs.json"
72-
RUN_ID=$(jq -er --arg sha "$GITHUB_SHA" --arg repo "$GITHUB_REPOSITORY" \
73-
--arg branch "$GITHUB_REF_NAME" '
86+
if ! RUN_ID=$(jq -er --arg sha "$RELEASE_SHA" --arg repo "$GITHUB_REPOSITORY" \
87+
--arg branch "$RELEASE_BRANCH" '
7488
[.workflow_runs[] | select(.head_sha == $sha and .head_branch == $branch
7589
and .head_repository.full_name == $repo
7690
and (.event == "push" or .event == "workflow_dispatch"))]
7791
| sort_by(.id) | last
7892
| select(.status == "completed" and .conclusion == "success") | .id
79-
' "$RUNNER_TEMP/quality-runs.json")
93+
' "$RUNNER_TEMP/quality-runs.json"); then
94+
echo "::error::No completed successful compatibility run for commit $RELEASE_SHA on $RELEASE_BRANCH. Check WordPress Compatibility & Plugin Check before rerunning a manual release."
95+
exit 1
96+
fi
8097
gh api --paginate --slurp \
8198
"repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/jobs?filter=latest&per_page=100" \
8299
> "$RUNNER_TEMP/quality-jobs.json"
83100
# A successful workflow with a skipped/missing gate is insufficient.
84-
jq -e '
101+
if ! jq -e '
85102
["WP Plugin Check (PHP 8.3)", "PHPCS (PHP 8.3)", "WP VIP CS (PHP 8.3)",
86103
"PHPMD (PHP 8.3)", "Psalm Static Analysis (PHP 8.3)",
87104
"Security Scan (PHP 8.3)", "PHPStan for WP (PHP 8.3)",
@@ -93,9 +110,15 @@ jobs:
93110
| all($expected[]; . as $name |
94111
([$jobs[] | select(.name == $name)] | length) == 1 and
95112
any($jobs[]; .name == $name and .status == "completed" and .conclusion == "success"))
96-
' "$RUNNER_TEMP/quality-jobs.json"
113+
' "$RUNNER_TEMP/quality-jobs.json"; then
114+
echo "::error::Quality run $RUN_ID has a missing, duplicate, unfinished or unsuccessful required job."
115+
exit 1
116+
fi
97117
if git rev-parse --verify "refs/tags/v$VERSION" >/dev/null 2>&1; then
98-
[[ $(git rev-parse "refs/tags/v$VERSION^{commit}") == "$GITHUB_SHA" ]]
118+
if [[ $(git rev-parse "refs/tags/v$VERSION^{commit}") != "$RELEASE_SHA" ]]; then
119+
echo "::error::Tag v$VERSION already points to a different commit."
120+
exit 1
121+
fi
99122
fi
100123
echo "Quality evidence: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_ID" \
101124
>> "$GITHUB_STEP_SUMMARY"
@@ -155,7 +178,7 @@ jobs:
155178
uses: softprops/action-gh-release@v3
156179
with:
157180
tag_name: v${{ steps.get_version.outputs.version }}
158-
target_commitish: ${{ github.sha }}
181+
target_commitish: ${{ env.RELEASE_SHA }}
159182
fail_on_unmatched_files: true
160183
name: Release v${{ steps.get_version.outputs.version }}
161184
body: |

0 commit comments

Comments
 (0)