Coverage for webapp/endpoints/publisher/builds.py: 48%

63 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 22:23 +0000

1# Standard library 

2import os 

3 

4# Packages 

5import flask 

6from canonicalwebteam.store_api.dashboard import Dashboard 

7 

8from requests.exceptions import HTTPError 

9 

10# Local 

11from webapp.helpers import api_publisher_session, launchpad 

12from webapp.api.github import GitHub, InvalidYAML 

13from webapp.decorators import login_required 

14 

15GITHUB_WEBHOOK_HOST_URL = os.getenv("GITHUB_WEBHOOK_HOST_URL") 

16 

17dashboard = Dashboard(api_publisher_session) 

18 

19 

20@login_required 

21def get_snap_build_page(snap_name, build_id): 

22 # If this fails, the page will 404 

23 dashboard.get_snap_info(flask.session, snap_name) 

24 return flask.render_template( 

25 "store/publisher.html", snap_name=snap_name, build_id=build_id 

26 ) 

27 

28 

29def validate_repo(github_token, snap_name, gh_owner, gh_repo): 

30 github = GitHub(github_token) 

31 result = {"success": True, "data": None, "error": None} 

32 yaml_location = github.get_snapcraft_yaml_location(gh_owner, gh_repo) 

33 default_branch = github.get_default_branch(gh_owner, gh_repo) 

34 

35 # The snapcraft.yaml is not present 

36 if not yaml_location: 

37 result["data"] = {"default_branch": default_branch} 

38 result["success"] = False 

39 result["error"] = { 

40 "type": "MISSING_YAML_FILE", 

41 "message": ( 

42 "Missing snapcraft.yaml: this repo needs a snapcraft.yaml " 

43 "file, so that Snapcraft can make it buildable, installable " 

44 "and runnable." 

45 ), 

46 } 

47 else: 

48 try: 

49 gh_snap_name = github.get_snapcraft_yaml_data( 

50 gh_owner, gh_repo 

51 ).get("name") 

52 

53 # The property name inside the yaml file doesn't match the snap 

54 if gh_snap_name is not None and gh_snap_name != snap_name: 

55 result["success"] = False 

56 result["error"] = { 

57 "type": "SNAP_NAME_DOES_NOT_MATCH", 

58 "message": ( 

59 "Name mismatch: the snapcraft.yaml uses the snap " 

60 f'name "{gh_snap_name}", but you\'ve registered' 

61 f' the name "{snap_name}". Update your ' 

62 "snapcraft.yaml to continue." 

63 ), 

64 "yaml_location": yaml_location, 

65 "gh_snap_name": gh_snap_name, 

66 } 

67 except InvalidYAML: 

68 result["success"] = False 

69 result["error"] = { 

70 "type": "INVALID_YAML_FILE", 

71 "message": ( 

72 "Invalid snapcraft.yaml: there was an issue parsing the " 

73 f"snapcraft.yaml for {snap_name}." 

74 ), 

75 } 

76 

77 return result 

78 

79 

80@login_required 

81def get_validate_repo(snap_name): 

82 details = dashboard.get_snap_info(flask.session, snap_name) 

83 

84 owner, repo = flask.request.args.get("repo").split("/") 

85 

86 return flask.jsonify( 

87 validate_repo( 

88 flask.session.get("github_auth_secret"), 

89 details["snap_name"], 

90 owner, 

91 repo, 

92 ) 

93 ) 

94 

95 

96@login_required 

97def post_build(snap_name): 

98 # Don't allow builds from no contributors 

99 account_snaps = dashboard.get_account_snaps(flask.session) 

100 

101 if snap_name not in account_snaps: 

102 return flask.jsonify( 

103 { 

104 "success": False, 

105 "error": { 

106 "type": "FORBIDDEN", 

107 "message": "You are not allowed to request " 

108 "builds for this snap", 

109 }, 

110 } 

111 ) 

112 

113 # Note: store authorization is established/repaired once, when the 

114 # GitHub repo is linked (or re-linked) via 

115 # webapp/publisher/snaps/build_views.py:post_snap_builds, which 

116 # completes a discharge round-trip through login.ubuntu.com for the 

117 # store's upload macaroon. Launchpad persists that authorization on 

118 # the snap itself, so it doesn't need to be redone here on every 

119 # build trigger. If a snap's authorization needs repairing, disconnect 

120 # and reconnect its repo to re-run that handshake. 

121 try: 

122 if launchpad.is_snap_building(snap_name): 

123 launchpad.cancel_snap_builds(snap_name) 

124 

125 build_id = launchpad.build_snap(snap_name) 

126 

127 except HTTPError as e: 

128 return flask.jsonify( 

129 { 

130 "success": False, 

131 "error": { 

132 "message": "An error happened building " 

133 "this snap, please try again." 

134 }, 

135 "details": e.response.text, 

136 "status_code": e.response.status_code, 

137 } 

138 ) 

139 

140 return flask.jsonify({"success": True, "build_id": build_id}) 

141 

142 

143@login_required 

144def post_disconnect_repo(snap_name): 

145 details = dashboard.get_snap_info(flask.session, snap_name) 

146 

147 lp_snap = launchpad.get_snap_by_store_name(snap_name) 

148 launchpad.delete_snap(details["snap_name"]) 

149 

150 # Try to remove the GitHub webhook if possible 

151 if flask.session.get("github_auth_secret"): 

152 github = GitHub(flask.session.get("github_auth_secret")) 

153 

154 try: 

155 gh_owner, gh_repo = lp_snap["git_repository_url"][19:].split("/") 

156 

157 old_hook = github.get_hook_by_url( 

158 gh_owner, 

159 gh_repo, 

160 f"{GITHUB_WEBHOOK_HOST_URL}api/{snap_name}/webhook/notify", 

161 ) 

162 

163 if old_hook: 

164 github.remove_hook( 

165 gh_owner, 

166 gh_repo, 

167 old_hook["id"], 

168 ) 

169 except HTTPError: 

170 pass 

171 

172 return flask.redirect( 

173 flask.url_for(".get_snap_builds", snap_name=snap_name) 

174 )