launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27960
[Merge] ~cjwatson/launchpad-buildd:ci-individual-results into launchpad-buildd:master
Colin Watson has proposed merging ~cjwatson/launchpad-buildd:ci-individual-results into launchpad-buildd:master.
Commit message:
Return results from individual CI jobs
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/+git/launchpad-buildd/+merge/414164
It's otherwise difficult for Launchpad to know how to record the results.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad-buildd:ci-individual-results into launchpad-buildd:master.
diff --git a/debian/changelog b/debian/changelog
index c35ca7e..b2bcf77 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+launchpad-buildd (207) UNRELEASED; urgency=medium
+
+ * Return results from individual CI jobs.
+
+ -- Colin Watson <cjwatson@xxxxxxxxxx> Thu, 13 Jan 2022 14:51:09 +0000
+
launchpad-buildd (206) bionic; urgency=medium
* Fix flake8 violations.
diff --git a/lpbuildd/ci.py b/lpbuildd/ci.py
index 9c70451..14ac7c6 100644
--- a/lpbuildd/ci.py
+++ b/lpbuildd/ci.py
@@ -24,6 +24,11 @@ RETCODE_SUCCESS = 0
RETCODE_FAILURE_INSTALL = 200
RETCODE_FAILURE_BUILD = 201
+# These must match the names of `RevisionStatusResult` enumeration items in
+# Launchpad.
+RESULT_SUCCEEDED = "SUCCEEDED"
+RESULT_FAILED = "FAILED"
+
class CIBuildState(DebianBuildState):
PREPARE = "PREPARE"
@@ -125,18 +130,20 @@ class CIBuildManager(BuildManagerProxyMixin, DebianBuildManager):
This state is repeated for each CI job in the pipeline.
"""
if retcode == RETCODE_SUCCESS:
- pass
- elif (retcode >= RETCODE_FAILURE_INSTALL and
- retcode <= RETCODE_FAILURE_BUILD):
- if not self.alreadyfailed:
- self._builder.log("Job %s failed." % self.current_job_id)
- self._builder.buildFail()
- self.alreadyfailed = True
+ result = RESULT_SUCCEEDED
else:
- if not self.alreadyfailed:
- self._builder.builderFail()
+ result = RESULT_FAILED
+ if (retcode >= RETCODE_FAILURE_INSTALL and
+ retcode <= RETCODE_FAILURE_BUILD):
+ self._builder.log("Job %s failed." % self.current_job_id)
+ if not self.alreadyfailed:
+ self._builder.buildFail()
+ else:
+ if not self.alreadyfailed:
+ self._builder.builderFail()
self.alreadyfailed = True
yield self.deferGatherResults(reap=False)
+ self.job_status[self.current_job_id]["result"] = result
if self.remaining_jobs and not self.alreadyfailed:
self.runNextJob()
else:
diff --git a/lpbuildd/tests/test_ci.py b/lpbuildd/tests/test_ci.py
index 1009a36..2cc1387 100644
--- a/lpbuildd/tests/test_ci.py
+++ b/lpbuildd/tests/test_ci.py
@@ -18,6 +18,10 @@ from lpbuildd.builder import get_build_path
from lpbuildd.ci import (
CIBuildManager,
CIBuildState,
+ RESULT_SUCCEEDED,
+ RESULT_FAILED,
+ RETCODE_FAILURE_BUILD,
+ RETCODE_SUCCESS,
)
from lpbuildd.tests.fakebuilder import FakeBuilder
from lpbuildd.tests.matchers import HasWaitingFiles
@@ -94,8 +98,9 @@ class TestCIBuildManagerIteration(TestCase):
self.assertFalse(self.builder.wasCalled("chrootFail"))
@defer.inlineCallbacks
- def expectRunJob(self, job_name, job_index, options=None):
- yield self.buildmanager.iterate(0)
+ def expectRunJob(self, job_name, job_index, options=None,
+ retcode=RETCODE_SUCCESS):
+ yield self.buildmanager.iterate(retcode)
self.assertEqual(CIBuildState.RUN_JOB, self.getState())
expected_command = [
"sharepath/bin/in-target", "in-target", "run-ci",
@@ -110,7 +115,7 @@ class TestCIBuildManagerIteration(TestCase):
self.assertFalse(self.builder.wasCalled("chrootFail"))
@defer.inlineCallbacks
- def test_iterate(self):
+ def test_iterate_success(self):
# The build manager iterates multiple CI jobs from start to finish.
args = {
"git_repository": "https://git.launchpad.test/~example/+git/ci",
@@ -148,6 +153,7 @@ class TestCIBuildManagerIteration(TestCase):
"output": {
"ci.whl": self.builder.waitingfiles["build:0/ci.whl"],
},
+ "result": RESULT_SUCCEEDED,
},
},
extra_status["jobs"])
@@ -179,6 +185,7 @@ class TestCIBuildManagerIteration(TestCase):
"output": {
"ci.whl": self.builder.waitingfiles["build:0/ci.whl"],
},
+ "result": RESULT_SUCCEEDED,
},
"test:0": {
"log": self.builder.waitingfiles["test:0.log"],
@@ -186,6 +193,7 @@ class TestCIBuildManagerIteration(TestCase):
"ci.tar.gz":
self.builder.waitingfiles["test:0/ci.tar.gz"],
},
+ "result": RESULT_SUCCEEDED,
},
},
extra_status["jobs"])
@@ -221,3 +229,80 @@ class TestCIBuildManagerIteration(TestCase):
shutil.rmtree(get_build_path(
self.buildmanager.home, self.buildmanager._buildid))
self.assertIn("jobs", self.buildmanager.status())
+
+ @defer.inlineCallbacks
+ def test_iterate_failure(self):
+ # The build manager records CI jobs that fail.
+ args = {
+ "git_repository": "https://git.launchpad.test/~example/+git/ci",
+ "git_path": "main",
+ "jobs": [("build", "0"), ("test", "0")],
+ }
+ expected_options = [
+ "--git-repository", "https://git.launchpad.test/~example/+git/ci",
+ "--git-path", "main",
+ ]
+ yield self.startBuild(args, expected_options)
+
+ # After preparation, start running the first job.
+ yield self.expectRunJob("build", "0")
+ self.buildmanager.backend.add_file(
+ "/build/output/build:0.log", b"I am a failing CI build job log.")
+
+ # If the first job fails, then the build fails here.
+ yield self.buildmanager.iterate(RETCODE_FAILURE_BUILD)
+ expected_command = [
+ "sharepath/bin/in-target", "in-target", "scan-for-processes",
+ "--backend=lxd", "--series=focal", "--arch=amd64", self.buildid,
+ ]
+ self.assertEqual(CIBuildState.RUN_JOB, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertNotEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
+ self.assertTrue(self.builder.wasCalled("buildFail"))
+ self.assertThat(self.builder, HasWaitingFiles.byEquality({
+ "build:0.log": b"I am a failing CI build job log.",
+ }))
+
+ # Output from the first job is visible in the status response.
+ extra_status = self.buildmanager.status()
+ self.assertEqual(
+ {
+ "build:0": {
+ "log": self.builder.waitingfiles["build:0.log"],
+ "result": RESULT_FAILED,
+ },
+ },
+ extra_status["jobs"])
+
+ # Control returns to the DebianBuildManager in the UMOUNT state.
+ self.buildmanager.iterateReap(self.getState(), 0)
+ expected_command = [
+ "sharepath/bin/in-target", "in-target", "umount-chroot",
+ "--backend=lxd", "--series=focal", "--arch=amd64", self.buildid,
+ ]
+ self.assertEqual(CIBuildState.UMOUNT, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
+ self.assertTrue(self.builder.wasCalled("buildFail"))
+
+ # If we iterate to the end of the build, then the extra status
+ # information is still present.
+ self.buildmanager.iterate(0)
+ expected_command = [
+ 'sharepath/bin/in-target', 'in-target', 'remove-build',
+ '--backend=lxd', '--series=focal', '--arch=amd64', self.buildid,
+ ]
+ self.assertEqual(CIBuildState.CLEANUP, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+ self.buildmanager.iterate(0)
+ self.assertFalse(self.builder.wasCalled('buildOK'))
+ self.assertTrue(self.builder.wasCalled('buildComplete'))
+ # remove-build would remove this in a non-test environment.
+ shutil.rmtree(get_build_path(
+ self.buildmanager.home, self.buildmanager._buildid))
+ self.assertIn("jobs", self.buildmanager.status())