Give a push-triggered job somewhere to report a failure
report_job_log live-smoke-test passed: live smoke test log line REGRESSION-CHECK-LIVE-SMOKE
report_job_log live-smoke-test passed: live smoke test log line REGRESSION-CHECK-LIVE-SMOKE
report_job_log.py only ever commented on an open PR, so a push to
development/main -- the shape of a publish pipeline, which has no
pull_request trigger -- had nowhere to post: the branch pushed to is the
merge's target, not the already-closed PR that produced it. Combined with
job logs 500ing on this Gitea, that made a push-triggered failure completely
invisible (weblib-viewer run 1769, diagnosed only by hardcoding --pr on a
throwaway commit).
A GitHub-style commit-comment endpoint, the first thing tried, does not
exist on this Gitea: 404, and absent from /swagger.v1.json entirely. What
does exist and was verified live: GET commits/{sha}/pull resolves the PR
that produced a given commit, open or closed, across both merge styles this
org uses -- so the closed PR the push actually came from can still be found
and commented on as before. When even that finds nothing (a commit that was
never part of any PR), fall back to a commit status, since that endpoint is
already proven to work here.
No CLI changes; existing callers are unaffected.
Closes #14
Co-authored-by: bit <bit@das-labor.org>
This commit is contained in:
+155
-1
@@ -42,10 +42,16 @@ ACTIONS_ENV = [
|
||||
|
||||
|
||||
class FakeGitea(BaseHTTPRequestHandler):
|
||||
"""Just enough of the API: list open PRs, and accept a comment."""
|
||||
"""Just enough of the API: list open PRs, resolve a commit's merged PR,
|
||||
accept a comment, and accept a commit status."""
|
||||
|
||||
comments = [] # class-level; reset per test
|
||||
statuses = []
|
||||
pulls = []
|
||||
# sha -> pull dict, mirroring GET .../commits/{sha}/pull. A sha absent
|
||||
# from this map 404s, the same shape Gitea returns for a commit that was
|
||||
# never part of any PR.
|
||||
pull_for_sha = {}
|
||||
|
||||
def _send(self, code, payload):
|
||||
raw = json.dumps(payload).encode()
|
||||
@@ -56,6 +62,14 @@ class FakeGitea(BaseHTTPRequestHandler):
|
||||
self.wfile.write(raw)
|
||||
|
||||
def do_GET(self):
|
||||
if "/commits/" in self.path and self.path.endswith("/pull"):
|
||||
sha = self.path.split("/commits/", 1)[1].rsplit("/pull", 1)[0]
|
||||
pull = type(self).pull_for_sha.get(sha)
|
||||
if pull is None:
|
||||
return self._send(
|
||||
404, {"message": "pull request does not exist "
|
||||
"[id: 0, issue_id: 0]"})
|
||||
return self._send(200, pull)
|
||||
if "/pulls" in self.path:
|
||||
return self._send(200, type(self).pulls)
|
||||
self._send(404, {"message": "no"})
|
||||
@@ -63,6 +77,16 @@ class FakeGitea(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
payload = json.loads(self.rfile.read(length) or b"{}")
|
||||
if "/statuses/" in self.path:
|
||||
type(self).statuses.append({
|
||||
"path": self.path,
|
||||
"auth": self.headers.get("Authorization"),
|
||||
"context": payload.get("context"),
|
||||
"state": payload.get("state"),
|
||||
"description": payload.get("description", ""),
|
||||
"target_url": payload.get("target_url"),
|
||||
})
|
||||
return self._send(201, {"id": 1, "state": payload.get("state")})
|
||||
type(self).comments.append({
|
||||
"path": self.path,
|
||||
"auth": self.headers.get("Authorization"),
|
||||
@@ -79,7 +103,9 @@ class ServerTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
FakeGitea.comments = []
|
||||
FakeGitea.statuses = []
|
||||
FakeGitea.pulls = []
|
||||
FakeGitea.pull_for_sha = {}
|
||||
self.server = HTTPServer(("127.0.0.1", 0), FakeGitea)
|
||||
self.host = "http://127.0.0.1:%d" % self.server.server_address[1]
|
||||
# `shutdown()` only takes effect on the next poll, so the default
|
||||
@@ -115,6 +141,10 @@ class ServerTestCase(unittest.TestCase):
|
||||
"""
|
||||
return FakeGitea.comments
|
||||
|
||||
def posted_statuses(self):
|
||||
"""Same idea as `posted()`, for the commit-status fallback."""
|
||||
return FakeGitea.statuses
|
||||
|
||||
def run_main(self, *argv):
|
||||
"""The entry point in-process, so a traceback is readable."""
|
||||
out = io.StringIO()
|
||||
@@ -293,6 +323,130 @@ class TestEndToEnd(ServerTestCase):
|
||||
self.assertIn("--status", proc.stdout)
|
||||
|
||||
|
||||
# A commit that landed by merging a now-closed PR into development/main --
|
||||
# weblib-ci#14's actual failure mode. A real 40-char sha, because
|
||||
# pull_request_for_sha silently treats a truncated one the same as "no PR".
|
||||
PUSH_SHA = "c9d6db67d9c20f8ab133f4fb5fea359e9c474274"
|
||||
|
||||
|
||||
class TestPushWithNoOpenPR(ServerTestCase):
|
||||
"""A `push` event: no event payload, no GITHUB_HEAD_REF, and the pushed
|
||||
branch (GITHUB_REF_NAME) is the *target* of a merge, not a PR head -- the
|
||||
exact shape of weblib-ci#14 (weblib-viewer run 1769, PR#27 merged into
|
||||
`development`)."""
|
||||
|
||||
def _run(self, *argv, env=None):
|
||||
full = dict(os.environ)
|
||||
full.pop("GITEA_HOST", None)
|
||||
full.update({
|
||||
"GITHUB_REPOSITORY": "weblib/weblib-ci",
|
||||
"GITHUB_SERVER_URL": self.host,
|
||||
"GITEA_TOKEN": "t0ken",
|
||||
"GITHUB_JOB": "publish",
|
||||
"GITHUB_SHA": PUSH_SHA,
|
||||
"GITHUB_REF_NAME": "development",
|
||||
})
|
||||
full.update(env or {})
|
||||
return subprocess.run([sys.executable, SCRIPT, self.log, *argv],
|
||||
capture_output=True, text=True, env=full,
|
||||
cwd=HERE)
|
||||
|
||||
def test_falls_back_to_the_pr_the_push_merged(self):
|
||||
"""No open PR has `development` as its head -- it never does -- but
|
||||
the merge commit itself resolves to the PR that produced it, even
|
||||
though that PR is now closed."""
|
||||
FakeGitea.pulls = []
|
||||
FakeGitea.pull_for_sha = {PUSH_SHA: {"number": 27, "state": "closed"}}
|
||||
|
||||
proc = self._run()
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("reported to weblib/weblib-ci#27", proc.stdout)
|
||||
|
||||
posted = self.posted()
|
||||
self.assertEqual(len(posted), 1, posted)
|
||||
self.assertEqual(posted[0]["path"],
|
||||
"/api/v1/repos/weblib/weblib-ci/issues/27/comments")
|
||||
self.assertIn("DISTINCTIVE-LINE", posted[0]["body"])
|
||||
self.assertEqual(self.posted_statuses(), [],
|
||||
"a resolvable PR must not also get a status")
|
||||
|
||||
def test_branch_lookup_still_wins_when_it_applies(self):
|
||||
"""Ordering: an *open* PR whose head matches the branch is found
|
||||
first, so the (potentially stale) sha lookup is never even tried."""
|
||||
FakeGitea.pulls = [{"number": 5, "head": {"ref": "development"}}]
|
||||
FakeGitea.pull_for_sha = {PUSH_SHA: {"number": 999}}
|
||||
|
||||
proc = self._run()
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("reported to weblib/weblib-ci#5", proc.stdout)
|
||||
self.assertNotIn("999", proc.stdout)
|
||||
|
||||
def test_no_pr_anywhere_reports_a_commit_status(self):
|
||||
"""Not even the sha lookup finds anything -- a commit that was never
|
||||
part of any PR. This must still report *somewhere*, not silently
|
||||
exit 0 the way #14 did."""
|
||||
FakeGitea.pulls = []
|
||||
FakeGitea.pull_for_sha = {}
|
||||
|
||||
proc = self._run()
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn(f"reported to weblib/weblib-ci@{PUSH_SHA[:8]}",
|
||||
proc.stdout)
|
||||
self.assertEqual(self.posted(), [], "no PR exists to comment on")
|
||||
|
||||
statuses = self.posted_statuses()
|
||||
self.assertEqual(len(statuses), 1, statuses)
|
||||
s = statuses[0]
|
||||
self.assertEqual(s["path"],
|
||||
f"/api/v1/repos/weblib/weblib-ci/statuses/{PUSH_SHA}")
|
||||
self.assertEqual(s["auth"], "token t0ken")
|
||||
self.assertEqual(s["state"], "failure")
|
||||
self.assertIn("publish", s["description"])
|
||||
self.assertIn("DISTINCTIVE-LINE", s["description"])
|
||||
self.assertIn(PUSH_SHA, s["target_url"])
|
||||
|
||||
def test_commit_status_state_follows_the_reported_outcome(self):
|
||||
FakeGitea.pulls = []
|
||||
FakeGitea.pull_for_sha = {}
|
||||
|
||||
for status, want_state in [("passed", "success"),
|
||||
("cancelled", "warning"),
|
||||
("skipped", "skipped"),
|
||||
("neutral", "error")]:
|
||||
with self.subTest(status=status):
|
||||
FakeGitea.statuses = []
|
||||
proc = self._run("--status", status)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual(self.posted_statuses()[0]["state"], want_state)
|
||||
|
||||
def test_no_sha_and_no_pr_does_not_crash_or_post_anything(self):
|
||||
"""The one case with nowhere at all to report: still exits 0 and
|
||||
says so, rather than raising or posting a status with an empty sha
|
||||
in the URL."""
|
||||
FakeGitea.pulls = []
|
||||
FakeGitea.pull_for_sha = {}
|
||||
|
||||
proc = self._run(env={"GITHUB_SHA": "", "GITHUB_REF_NAME": "development"})
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("nothing to report", proc.stdout)
|
||||
self.assertEqual(self.posted(), [])
|
||||
self.assertEqual(self.posted_statuses(), [])
|
||||
|
||||
def test_existing_pr_event_path_is_unaffected(self):
|
||||
"""The ordinary pull_request-event path -- unrelated to any of the
|
||||
push-triggered fallback -- still goes straight to the comment, and
|
||||
never touches the sha or status endpoints."""
|
||||
path = os.path.join(self.mkdtemp(), "event.json")
|
||||
with open(path, "w") as fh:
|
||||
json.dump({"pull_request": {"number": 11}}, fh)
|
||||
FakeGitea.pull_for_sha = {PUSH_SHA: {"number": 999}} # must be ignored
|
||||
|
||||
proc = self._run(env={"GITHUB_EVENT_PATH": path})
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("reported to weblib/weblib-ci#11", proc.stdout)
|
||||
self.assertEqual(self.posted_statuses(), [])
|
||||
|
||||
|
||||
class TestStatusPhrase(unittest.TestCase):
|
||||
"""The unit under the header, without a server."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user