Merge ~cjwatson/launchpad:ci-build-files into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 6a3f8f39b5f5842b812850c76a838d215386b767
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:ci-build-files
Merge into: launchpad:master
Diff against target: 253 lines (+128/-13)
6 files modified
lib/lp/code/browser/cibuild.py (+21/-1)
lib/lp/code/browser/tests/test_cibuild.py (+50/-1)
lib/lp/code/interfaces/cibuild.py (+8/-0)
lib/lp/code/model/cibuild.py (+22/-11)
lib/lp/code/templates/cibuild-index.pt (+20/-0)
lib/lp/services/librarian/browser.py (+7/-0)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+437673@code.launchpad.net

Commit message

Show built files on CIBuild:+index

Description of the change

Most build types show links to their output files on their index page, and it was odd that CI builds didn't; it also made certain kinds of debugging more difficult.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍 with a question.

review: Approve
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/browser/cibuild.py b/lib/lp/code/browser/cibuild.py
2index 187f5ef..a1018e1 100644
3--- a/lib/lp/code/browser/cibuild.py
4+++ b/lib/lp/code/browser/cibuild.py
5@@ -13,7 +13,11 @@ from zope.interface import Interface
6
7 from lp.app.browser.launchpadform import LaunchpadFormView, action
8 from lp.code.interfaces.cibuild import ICIBuild
9-from lp.services.librarian.browser import FileNavigationMixin
10+from lp.services.librarian.browser import (
11+ FileNavigationMixin,
12+ ProxiedLibraryFileAlias,
13+)
14+from lp.services.propertycache import cachedproperty
15 from lp.services.webapp import (
16 ContextMenu,
17 Link,
18@@ -77,6 +81,22 @@ class CIBuildView(LaunchpadFormView):
19
20 page_title = label
21
22+ @cachedproperty
23+ def files(self):
24+ """Return `LibraryFileAlias`es for files produced by this build."""
25+ if not self.context.was_built:
26+ return None
27+
28+ return [
29+ ProxiedLibraryFileAlias(artifact.library_file, artifact)
30+ for artifact in self.context.getArtifacts()
31+ if not artifact.library_file.deleted
32+ ]
33+
34+ @cachedproperty
35+ def has_files(self):
36+ return bool(self.files)
37+
38
39 class CIBuildRetryView(LaunchpadFormView):
40 """View for retrying a CI build."""
41diff --git a/lib/lp/code/browser/tests/test_cibuild.py b/lib/lp/code/browser/tests/test_cibuild.py
42index 2390997..3c5d7bf 100644
43--- a/lib/lp/code/browser/tests/test_cibuild.py
44+++ b/lib/lp/code/browser/tests/test_cibuild.py
45@@ -11,6 +11,7 @@ from fixtures import FakeLogger
46 from storm.locals import Store
47 from zope.component import getUtility
48 from zope.security.interfaces import Unauthorized
49+from zope.security.proxy import removeSecurityProxy
50 from zope.testbrowser.browser import LinkNotFoundError
51
52 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
53@@ -23,12 +24,13 @@ from lp.testing import (
54 login,
55 person_logged_in,
56 )
57-from lp.testing.layers import DatabaseFunctionalLayer
58+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
59 from lp.testing.pages import (
60 extract_text,
61 find_main_content,
62 find_tags_by_class,
63 )
64+from lp.testing.views import create_initialized_view
65
66
67 class TestCanonicalUrlForCIBuild(TestCaseWithFactory):
68@@ -224,3 +226,50 @@ class TestCIBuildOperations(BrowserTestCase):
69 build = self.makeBuildingRecipe()
70 browser = self.getViewBrowser(build.builder, no_login=True)
71 self.assertIn("tail of the log", browser.contents)
72+
73+
74+class TestCIBuildView(TestCaseWithFactory):
75+
76+ layer = LaunchpadFunctionalLayer
77+
78+ def test_files(self):
79+ # CIBuildView.files returns all the associated files.
80+ build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT)
81+ reports = [
82+ build.getOrCreateRevisionStatusReport(job_id)
83+ for job_id in ("build:0", "build:1")
84+ ]
85+ for report in reports:
86+ # Deliberately use identical filenames for each artifact, since
87+ # that's the hardest case.
88+ removeSecurityProxy(report).attach(
89+ "package.tar.gz", report.title.encode()
90+ )
91+ artifacts = build.getArtifacts()
92+ self.assertContentEqual(
93+ reports, {artifact.report for artifact in artifacts}
94+ )
95+ build_view = create_initialized_view(build, "+index")
96+ self.assertEqual(
97+ [
98+ "%s/+files/%s"
99+ % (canonical_url(artifact), artifact.library_file.filename)
100+ for artifact in artifacts
101+ ],
102+ [lfa.http_url for lfa in build_view.files],
103+ )
104+ # Deleted files won't be included.
105+ self.assertFalse(artifacts[1].library_file.deleted)
106+ removeSecurityProxy(artifacts[1].library_file).content = None
107+ self.assertTrue(artifacts[1].library_file.deleted)
108+ build_view = create_initialized_view(build, "+index")
109+ self.assertEqual(
110+ [
111+ "%s/+files/%s"
112+ % (
113+ canonical_url(artifacts[0]),
114+ artifacts[0].library_file.filename,
115+ )
116+ ],
117+ [lfa.http_url for lfa in build_view.files],
118+ )
119diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py
120index 5c7725f..9599b75 100644
121--- a/lib/lp/code/interfaces/cibuild.py
122+++ b/lib/lp/code/interfaces/cibuild.py
123@@ -201,6 +201,14 @@ class ICIBuildView(IPackageBuildView, IPrivacy):
124 :return: The corresponding `ILibraryFileAlias`.
125 """
126
127+ def getArtifacts():
128+ """Return `IRevisionStatusArtifact`s produced by this build.
129+
130+ :return: A result set of `IRevisionStatusArtifact`s, ordered by
131+ filename then artifact ID, with their associated
132+ `ILibraryFileAlias`es preloaded.
133+ """
134+
135 @export_read_operation()
136 @operation_for_version("devel")
137 def getFileUrls():
138diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
139index 0bfc773..c06e7a6 100644
140--- a/lib/lp/code/model/cibuild.py
141+++ b/lib/lp/code/model/cibuild.py
142@@ -9,7 +9,7 @@ __all__ = [
143
144 from copy import copy
145 from datetime import timedelta
146-from operator import attrgetter, itemgetter
147+from operator import itemgetter
148
149 import pytz
150 from lazr.lifecycle.event import ObjectCreatedEvent
151@@ -49,12 +49,13 @@ from lp.code.interfaces.cibuild import (
152 )
153 from lp.code.interfaces.githosting import IGitHostingClient
154 from lp.code.interfaces.gitrepository import IGitRepository
155-from lp.code.interfaces.revisionstatus import (
156- IRevisionStatusArtifactSet,
157- IRevisionStatusReportSet,
158-)
159+from lp.code.interfaces.revisionstatus import IRevisionStatusReportSet
160 from lp.code.model.gitref import GitRef
161 from lp.code.model.lpcraft import load_configuration
162+from lp.code.model.revisionstatus import (
163+ RevisionStatusArtifact,
164+ RevisionStatusReport,
165+)
166 from lp.registry.interfaces.pocket import PackagePublishingPocket
167 from lp.registry.interfaces.series import SeriesStatus
168 from lp.registry.interfaces.sourcepackage import SourcePackageType
169@@ -478,15 +479,25 @@ class CIBuild(PackageBuildMixin, StormBase):
170
171 raise NotFoundError(filename)
172
173- def getFileUrls(self):
174- artifacts = getUtility(IRevisionStatusArtifactSet).findByCIBuild(self)
175- load_related(LibraryFileAlias, artifacts, ["library_file_id"])
176- artifacts = sorted(
177- artifacts, key=attrgetter("library_file.filename", "id")
178+ def getArtifacts(self):
179+ """See `ICIBuild`."""
180+ artifacts = (
181+ IStore(self)
182+ .find(
183+ (RevisionStatusArtifact, LibraryFileAlias),
184+ RevisionStatusReport.ci_build == self,
185+ RevisionStatusArtifact.report == RevisionStatusReport.id,
186+ RevisionStatusArtifact.library_file == LibraryFileAlias.id,
187+ )
188+ .order_by(LibraryFileAlias.filename, RevisionStatusArtifact.id)
189 )
190+ return DecoratedResultSet(artifacts, result_decorator=itemgetter(0))
191+
192+ def getFileUrls(self):
193+ """See `ICIBuild`."""
194 return [
195 ProxiedLibraryFileAlias(artifact.library_file, artifact).http_url
196- for artifact in artifacts
197+ for artifact in self.getArtifacts()
198 ]
199
200 def verifySuccessfulUpload(self) -> bool:
201diff --git a/lib/lp/code/templates/cibuild-index.pt b/lib/lp/code/templates/cibuild-index.pt
202index 35ef0cf..790b651 100644
203--- a/lib/lp/code/templates/cibuild-index.pt
204+++ b/lib/lp/code/templates/cibuild-index.pt
205@@ -33,6 +33,10 @@
206
207 </div> <!-- yui-g -->
208
209+ <div id="files" class="portlet" tal:condition="view/has_files">
210+ <div metal:use-macro="template/macros/files"/>
211+ </div>
212+
213 <div id="buildlog" class="portlet"
214 tal:condition="context/status/enumvalue:BUILDING">
215 <div metal:use-macro="template/macros/buildlog"/>
216@@ -162,6 +166,22 @@
217 </div>
218 </metal:macro>
219
220+ <metal:macro define-macro="files">
221+ <tal:comment replace="nothing">
222+ Files section.
223+ </tal:comment>
224+ <h2>Built files</h2>
225+ <p>Files resulting from this build:</p>
226+ <ul>
227+ <li tal:repeat="file view/files">
228+ <a class="sprite download"
229+ tal:content="file/filename"
230+ tal:attributes="href file/http_url"/>
231+ (<span tal:replace="file/content/filesize/fmt:bytes"/>)
232+ </li>
233+ </ul>
234+ </metal:macro>
235+
236 <metal:macro define-macro="buildlog">
237 <tal:comment replace="nothing">
238 Buildlog section.
239diff --git a/lib/lp/services/librarian/browser.py b/lib/lp/services/librarian/browser.py
240index bd42c26..d364b68 100644
241--- a/lib/lp/services/librarian/browser.py
242+++ b/lib/lp/services/librarian/browser.py
243@@ -149,3 +149,10 @@ class ProxiedLibraryFileAlias:
244 url_path_quote(self.context.filename.encode("utf-8")),
245 )
246 return url
247+
248+ # XXX cjwatson 2023-02-23: For plain `LibraryFileAlias`, callers prefer
249+ # to use `getURL` over `http_url`, because that returns an HTTPS URL
250+ # where appropriate. However, if you try to do that with a
251+ # `ProxiedLibraryFileAlias`, it returns a URL pointing directly to the
252+ # librarian rather than proxying via the webapp. To avoid this gotcha,
253+ # perhaps we should implement `getURL` here as well.

Subscribers

People subscribed via source and target branches

to status/vote changes: