def add_release(body): # we must require scope which depends on XPI type xpi_type = _xpi_type(body["revision"], body["xpi_name"]) required_permission = f"{SCOPE_PREFIX}/add_release/xpi/{xpi_type}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort(401, f"required permission: {required_permission}, user permissions: {user_permissions}") session = current_app.db.session xpi = XPI(name=body["xpi_name"], revision=body["xpi_revision"], version=body["xpi_version"]) # project name used in the TC routes and matches the repo name project = current_app.config.get("XPI_MANIFEST_REPO") release = XPIRelease(revision=body["revision"], xpi=xpi, build_number=body["build_number"], status="scheduled", xpi_type=xpi_type, project=project) try: common_input = {"build_number": release.build_number, "xpi_name": release.xpi_name, "revision": release.xpi_revision, "version": release.xpi_version} release.phases = generate_phases(release, common_input, verify_supported_flavors=False) session.add(release) session.commit() except UnsupportedFlavor as e: raise BadRequest(description=e.description) except (IntegrityError, TaskclusterRestFailure) as e: # Report back Taskcluster and SQL failures for better visibility of the # actual issue. Usually it happens when we cannot find the indexed # task or a duplicate release request accordingly. abort(400, str(e)) logger.info("New release of %s", release.name) notify_via_matrix("xpi", f"New release of {release.name}") return release.json, 201
def abandon_release(name): session = current_app.db.session try: release = session.query(Release).filter(Release.name == name).one() # we must require scope which depends on product required_permission = f"{SCOPE_PREFIX}/abandon_release/{release.product}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) # Cancel all submitted task groups first for phase in filter(lambda x: x.submitted and not x.skipped, release.phases): try: actions = fetch_artifact(phase.task_id, "public/actions.json") parameters = fetch_artifact(phase.task_id, "public/parameters.yml") except ArtifactNotFound: logger.info("Ignoring not completed action task %s", phase.task_id) continue hook = generate_action_hook(task_group_id=phase.task_id, action_name="cancel-all", actions=actions, parameters=parameters, input_={}) hooks = get_service("hooks") client_id = hooks.options["credentials"]["clientId"].decode( "utf-8") hook["context"]["clientId"] = client_id hook_payload_rendered = render_action_hook( payload=hook["hook_payload"], context=hook["context"], delete_params=[ "existing_tasks", "release_history", "release_partner_config" ]) logger.info("Cancel phase %s by hook %s with payload: %s", phase.name, hook["hook_id"], hook_payload_rendered) res = hooks.triggerHook(hook["hook_group_id"], hook["hook_id"], hook_payload_rendered) logger.debug("Done: %s", res) release.status = "aborted" session.commit() release_json = release.json except NoResultFound: abort(404) notify_via_irc( release.product, f"Release {release.product} {release.version} build{release.build_number} was just canceled." ) return release_json
def add_release(body): # we must require scope which depends on product required_permission = f'{SCOPE_PREFIX}/add_release/{body["product"]}' if not current_user.has_permissions(required_permission): user_permissions = ', '.join(current_user.get_permissions()) abort(401, f'required permission: {required_permission}, user permissions: {user_permissions}') session = current_app.db.session product = body['product'] r = Release( product=product, version=body['version'], branch=body['branch'], revision=body['revision'], build_number=body['build_number'], release_eta=body.get('release_eta'), status='scheduled', partial_updates=body.get('partial_updates'), product_key=body.get('product_key'), ) try: r.generate_phases( partner_urls=current_app.config.get('PARTNERS_URL'), github_token=current_app.config.get('GITHUB_TOKEN'), ) session.add(r) session.commit() release = r.json except UnsupportedFlavor as e: raise BadRequest(description=e.description) notify_via_irc(product, f'New release ({product} {r.version} build{r.build_number}) was just created.') return release, 201
def schedule_phase(name, phase): session = current_app.db.session phase = session.query(XPIPhase).filter( XPIRelease.id == XPIPhase.release_id).filter( XPIRelease.name == name).filter( XPIPhase.name == phase).first_or_404() # we must require scope which depends on XPI type xpi_type = _xpi_type(phase.release.revision, phase.release.xpi_name) required_permission = f"{SCOPE_PREFIX}/schedule_phase/xpi/{xpi_type}/{phase.name}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) phase = do_schedule_phase(session, phase) url = taskcluster_urls.ui(get_root_url(), f"/tasks/groups/{phase.task_id}") logger.info("Phase %s of %s started by %s. - %s", phase.name, phase.release.name, phase.completed_by, url) notify_via_matrix( "xpi", f"Phase {phase.name} of {phase.release.name} started by {phase.completed_by}. - {url}" ) return phase.json
def abandon_release(name): session = current_app.db.session try: release = session.query(Release).filter(Release.name == name).one() # we must require scope which depends on product required_permission = f'{SCOPE_PREFIX}/abandon_release/{release.product}' if not current_user.has_permissions(required_permission): user_permissions = ', '.join(current_user.get_permissions()) abort( 401, f'required permission: {required_permission}, user permissions: {user_permissions}' ) # Cancel all submitted task groups first for phase in filter(lambda x: x.submitted, release.phases): try: actions = fetch_actions_json(phase.task_id) except ActionsJsonNotFound: logger.info('Ignoring not completed action task %s', phase.task_id) continue hook = generate_action_hook( task_group_id=phase.task_id, action_name='cancel-all', actions=actions, input_={}, ) hooks = get_service('hooks') client_id = hooks.options['credentials']['clientId'].decode( 'utf-8') hook['context']['clientId'] = client_id hook_payload_rendered = render_action_hook( payload=hook['hook_payload'], context=hook['context'], delete_params=[ 'existing_tasks', 'release_history', 'release_partner_config' ], ) logger.info('Cancel phase %s by hook %s with payload: %s', phase.name, hook['hook_id'], hook_payload_rendered) res = hooks.triggerHook(hook['hook_group_id'], hook['hook_id'], hook_payload_rendered) logger.debug('Done: %s', res) release.status = 'aborted' session.commit() release_json = release.json except NoResultFound: abort(404) notify_via_irc( release.product, f'Release {release.product} {release.version} build{release.build_number} was just canceled.' ) return release_json
def schedule_phase(name, phase): session = current_app.db.session try: phase = session.query(Phase).filter( Release.id == Phase.release_id).filter( Release.name == name).filter(Phase.name == phase).one() except NoResultFound: abort(404) # we must require scope which depends on product required_permission = f"{SCOPE_PREFIX}/schedule_phase/{phase.release.product}/{phase.name}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) if phase.submitted: abort(409, "Already submitted!") for signoff in phase.signoffs: if not signoff.signed: abort(400, "Pending signoffs") hook = phase.task_json if "hook_payload" not in hook: raise ValueError("Action tasks are not supported") hooks = get_service("hooks") client_id = hooks.options["credentials"]["clientId"].decode("utf-8") extra_context = {"clientId": client_id} result = hooks.triggerHook( hook["hook_group_id"], hook["hook_id"], phase.rendered_hook_payload(extra_context=extra_context)) phase.task_id = result["status"]["taskId"] phase.submitted = True phase.completed_by = current_user.get_id() completed = datetime.datetime.utcnow() phase.completed = completed if all([ph.submitted for ph in phase.release.phases]): phase.release.status = "shipped" phase.release.completed = completed session.commit() root_url = hooks.options["rootUrl"] url = f"{root_url}/tasks/groups/{phase.task_id}" # TODO: Remove the condition after migration if root_url == "https://taskcluster.net": url = "https://tools.taskcluster.net/groups/{phase.task_id}" notify_via_irc( phase.release.product, f"Phase {phase.name} was just scheduled for release {phase.release.product} {phase.release.version} build{phase.release.build_number} - {url}", ) return phase.json
def abandon_release(name): session = current_app.db.session release = session.query(Release).filter( Release.name == name).first_or_404() # we must require scope which depends on product required_permission = f"{SCOPE_PREFIX}/abandon_release/{release.product}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) # Cancel all submitted task groups first for phase in filter(lambda x: x.submitted and not x.skipped, release.phases): try: actions = get_actions(phase.task_id) parameters = get_parameters(phase.task_id) cancel_action = find_action("cancel-all", actions) if not cancel_action: logger.info( "%s %s does not have `cancel-all` action, skipping...", release.name, phase.name) continue except ArtifactNotFound: logger.info("Ignoring not completed action task %s", phase.task_id) continue hook = generate_action_hook(task_group_id=phase.task_id, action_name="cancel-all", actions=actions, parameters=parameters, input_={}) hooks = get_service("hooks") client_id = hooks.options["credentials"]["clientId"].decode("utf-8") hook["context"]["clientId"] = client_id hook_payload_rendered = render_action_hook( payload=hook["hook_payload"], context=hook["context"], delete_params=[ "existing_tasks", "release_history", "release_partner_config" ]) logger.info("Cancel phase %s by hook %s with payload: %s", phase.name, hook["hook_id"], hook_payload_rendered) try: result = hooks.triggerHook(hook["hook_group_id"], hook["hook_id"], hook_payload_rendered) logger.debug("Done: %s", result) except TaskclusterRestFailure as e: abort(400, str(e)) release.status = "aborted" session.commit() logger.info("Canceled release %s", release.name) notify_via_matrix(release.product, f"Release {release.name} was just canceled.") return release.json
def _require_auth(): required_permission = f"{SCOPE_PREFIX}/github" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" )
def schedule_phase(name, phase): session = current_app.db.session try: phase = session.query(Phase) \ .filter(Release.id == Phase.release_id) \ .filter(Release.name == name) \ .filter(Phase.name == phase).one() except NoResultFound: abort(404) # we must require scope which depends on product required_permission = f'{SCOPE_PREFIX}/schedule_phase/{phase.release.product}/{phase.name}' if not current_user.has_permissions(required_permission): user_permissions = ', '.join(current_user.get_permissions()) abort( 401, f'required permission: {required_permission}, user permissions: {user_permissions}' ) if phase.submitted: abort(409, 'Already submitted!') for signoff in phase.signoffs: if not signoff.signed: abort(400, 'Pending signoffs') hook = phase.task_json if 'hook_payload' not in hook: raise ValueError('Action tasks are not supported') hooks = get_service('hooks') client_id = hooks.options['credentials']['clientId'].decode('utf-8') extra_context = {'clientId': client_id} result = hooks.triggerHook( hook['hook_group_id'], hook['hook_id'], phase.rendered_hook_payload(extra_context=extra_context), ) phase.task_id = result['status']['taskId'] phase.submitted = True phase.completed_by = current_user.get_id() completed = datetime.datetime.utcnow() phase.completed = completed if all([ph.submitted for ph in phase.release.phases]): phase.release.status = 'shipped' phase.release.completed = completed session.commit() notify_via_irc( phase.release.product, f'Phase {phase.name} was just scheduled ' f'for release {phase.release.product} {phase.release.version} ' f'build{phase.release.build_number} - ' f'(https://tools.taskcluster.net/groups/{phase.task_id})') return phase.json
def phase_signoff(name, phase, body): session = current_app.db.session signoff = session.query(XPISignoff).filter( XPISignoff.uid == body).first_or_404() if signoff.signed: abort(409, "Already signed off") phase_obj = (session.query(XPIPhase).filter( XPIRelease.id == XPIPhase.release_id).filter( XPIRelease.name == name).filter( XPIPhase.name == phase).first_or_404()) # we must require scope which depends on product and phase name xpi_type = _xpi_type(phase_obj.release.revision, phase_obj.release.xpi_name) required_permission = f"{SCOPE_PREFIX}/phase_signoff/xpi/{xpi_type}/{signoff.phase.name}" if not current_user.has_permissions( required_permission) and not XPI_LAX_SIGN_OFF: user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) # Prevent the same user signing off for multiple signoffs users_ldap = current_user.get_ldap_groups() users_email = current_user.get_id() if users_email in [s.completed_by for s in phase_obj.signoffs] and not XPI_LAX_SIGN_OFF: abort(409, f"Already signed off by {users_email}") # signoff.permissions corresponds to the group in settings.py ldap_groups = current_app.config.get("LDAP_GROUPS", {}) ldap_group = ldap_groups.get(signoff.permissions, []) if not set(users_ldap).intersection( set(ldap_group)) and not XPI_LAX_SIGN_OFF: abort(401, f"User `{users_email}` is not in the `{signoff.permissions}`") signoff.completed = datetime.datetime.utcnow() signoff.signed = True signoff.completed_by = users_email session.commit() signoffs = [s.json for s in phase_obj.signoffs] # Schedule the phase when all signoffs are done if all([s.signed for s in phase_obj.signoffs]): schedule_phase(name, phase) release = phase_obj.release logger.info("Phase %s of %s signed off by %s", phase, release.name, users_email) notify_via_matrix( "xpi", f"Phase {phase} of {release.name} signed off by {users_email}") return dict(signoffs=signoffs)
def _require_scopes(self, scopes): response = self._require_login() if response is not None: return response with current_app.app_context(): user_scopes = current_user.get_permissions() if not scope_match(user_scopes, scopes): diffs = [', '.join(set(s).difference(user_scopes)) for s in scopes] # noqa logger.error('User {} misses some scopes: {}'.format(current_user.get_id(), ' OR '.join(diffs))) # noqa return abort(401)
def add_release(body): # we must require scope which depends on product required_permission = f'{SCOPE_PREFIX}/add_release/{body["product"]}' if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) product = body["product"] branch = body["branch"] product_disabled = branch in get_disabled_products().get(product, []) if current_user.type == AuthType.TASKCLUSTER and product_disabled: abort(401, "Taskcluster based submissions are disabled") session = current_app.db.session partial_updates = body.get("partial_updates") if partial_updates == "auto": if product not in [ Product.FIREFOX.value, Product.DEVEDITION.value ] or branch not in ["try", "releases/mozilla-beta", "projects/maple"]: abort(400, "Partial suggestion works for automated betas only") partial_updates = _suggest_partials(product=product, branch=branch) release = Release( branch=branch, build_number=body["build_number"], partial_updates=partial_updates, product_key=body.get("product_key"), product=product, release_eta=body.get("release_eta"), repo_url=body.get("repo_url", ""), revision=body["revision"], status="scheduled", version=body["version"], ) try: release.generate_phases() session.add(release) session.commit() except UnsupportedFlavor as e: raise BadRequest(description=e.description) except TaskclusterRestFailure as e: # Report back Taskcluster failure for better visibility of the actual # issue. Usually it happens when we cannot find the indexed task. abort(400, str(e)) logger.info("New release of %s", release.name) notify_via_irc(product, f"New release of {release.name}") return release.json, 201
def add_release(body): # we must require scope which depends on product required_permission = f'{SCOPE_PREFIX}/add_release/{body["product"]}' if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) product = body["product"] branch = body["branch"] product_disabled = branch in get_disabled_products().get(product, []) if current_user.type == AuthType.TASKCLUSTER and product_disabled: abort(401, "Taskcluster based submissions are disabled") session = current_app.db.session partial_updates = body.get("partial_updates") if partial_updates == "auto": if product not in [ Product.FIREFOX.value, Product.DEVEDITION.value ] or branch not in ["try", "releases/mozilla-beta", "projects/maple"]: abort(400, "Partial suggestion works for automated betas only") partial_updates = _suggest_partials(product=product, branch=branch) r = Release( product=product, version=body["version"], branch=branch, revision=body["revision"], build_number=body["build_number"], release_eta=body.get("release_eta"), status="scheduled", partial_updates=partial_updates, product_key=body.get("product_key"), ) try: r.generate_phases(partner_urls=current_app.config.get("PARTNERS_URL"), github_token=current_app.config.get("GITHUB_TOKEN")) session.add(r) session.commit() release = r.json except UnsupportedFlavor as e: raise BadRequest(description=e.description) notify_via_irc( product, f"New release ({product} {r.version} build{r.build_number}) was just created." ) return release, 201
def phase_signoff(name, phase, uid): session = current_app.db.session try: signoff = session.query(Signoff) \ .filter(Signoff.uid == uid).one() except NoResultFound: abort(404, 'Sign off does not exist') if signoff.signed: abort(409, 'Already signed off') # we must require scope which depends on product and phase name required_permission = f'{SCOPE_PREFIX}/phase_signoff/{signoff.phase.release.product}/{signoff.phase.name}' if not current_user.has_permissions(required_permission): user_permissions = ', '.join(current_user.get_permissions()) abort( 401, f'required permission: {required_permission}, user permissions: {user_permissions}' ) try: # Prevent the same user signing off for multiple signoffs phase_obj = session.query(Phase) \ .filter(Release.id == Phase.release_id) \ .filter(Release.name == name) \ .filter(Phase.name == phase).one() except NoResultFound: abort(404, 'Phase not found') who = current_user.get_id() if who in [s.completed_by for s in phase_obj.signoffs]: abort(409, f'Already signed off by {who}') signoff.completed = datetime.datetime.utcnow() signoff.signed = True signoff.completed_by = who session.commit() signoffs = [s.json for s in phase_obj.signoffs] # Schedule the phase when all signoffs are done if all([s.signed for s in phase_obj.signoffs]): schedule_phase(name, phase) r = phase_obj.release notify_via_irc( r.product, f'{phase} of {r.product} {r.version} build{r.build_number} signed off by {who}.' ) return dict(signoffs=signoffs)
def abandon_release_xpi(name): session = current_app.db.session release = session.query(XPIRelease).filter(XPIRelease.name == name).first_or_404() # we must require scope which depends on XPI type xpi_type = _xpi_type(release.revision, release.xpi_name) required_permission = f"{SCOPE_PREFIX}/abandon_release/xpi/{xpi_type}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort(401, f"required permission: {required_permission}, user permissions: {user_permissions}") # XPI doesn't support the "cancel-all" action task, just mark as aborted release.status = "aborted" session.commit() logger.info("Canceled release %s", release.name) return release.json
def phase_signoff(name, phase, body): session = current_app.db.session signoff = session.query(Signoff).filter(Signoff.uid == body).first_or_404() if signoff.signed: abort(409, "Already signed off") # we must require scope which depends on product and phase name required_permission = f"{SCOPE_PREFIX}/phase_signoff/{signoff.phase.release.product}/{signoff.phase.name}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) # Prevent the same user signing off for multiple signoffs phase_obj = session.query(Phase).filter( Release.id == Phase.release_id).filter(Release.name == name).filter( Phase.name == phase).first_or_404() who = current_user.get_id() if who in [s.completed_by for s in phase_obj.signoffs]: abort(409, f"Already signed off by {who}") # signoff.permissions corresponds to the group in settings.py groups = current_app.config.get("GROUPS", {}) group = groups.get(signoff.permissions, []) if who not in group: abort(401, f"User `{who}` is not in the `{signoff.permissions}` group") signoff.completed = datetime.datetime.utcnow() signoff.signed = True signoff.completed_by = who session.commit() signoffs = [s.json for s in phase_obj.signoffs] # Schedule the phase when all signoffs are done if all([s.signed for s in phase_obj.signoffs]): schedule_phase(name, phase) release = phase_obj.release logger.info("Phase %s of %s signed off by %s", phase, release.name, who) notify_via_matrix(release.product, f"{phase} of {release.name} signed off by {who}.") return dict(signoffs=signoffs)
def enable_product(product, branch): session = current_app.db.session required_permission = f"{SCOPE_PREFIX}/enable_product/{product}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) dp = session.query(DisabledProduct).filter( DisabledProduct.product == product).filter( DisabledProduct.branch == branch).first_or_404() session.delete(dp) session.commit() logger.info("Enabled %s on branch %s", product, branch) return 200
def _authenticate(self): app = self.get_app() assert app, "Please initialize your application into Flask-RBAC." assert self._role_model, "Please set role model before authenticate." assert self._user_model, "Please set user model before authenticate." assert self._user_loader, "Please set user loader before authenticate." current_user = self._user_loader() # Compatible with flask-login anonymous user if hasattr(current_user, '_get_current_object'): current_user = current_user._get_current_object() if (current_user is not None and not isinstance(current_user, (self._user_model, anonymous_model))): raise TypeError("%s is not an instance of %s" % (current_user, self._user_model.__class__)) endpoint = request.endpoint resource = app.view_functions.get(endpoint, None) if not resource: abort(404) method = request.method if not self._is_user_permission: if not hasattr(current_user, 'get_roles'): roles = [anonymous] else: roles = current_user.get_roles() permit = self._check_permission(roles, method, resource) else: # permission if not hasattr(current_user, 'get_permissions'): permissions = [] else: permissions = current_user.get_permissions() permit = self._check_permission2(permissions, endpoint) if not permit: return self._deny_hook()
def disable_product(body): product = body["product"] branch = body["branch"] required_permission = f"{SCOPE_PREFIX}/disable_product/{product}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) session = current_app.db.session dp = DisabledProduct(product=product, branch=branch) session.add(dp) session.commit() return 200
def enable_product(product, branch): session = current_app.db.session required_permission = f"{SCOPE_PREFIX}/enable_product/{product}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) try: dp = session.query(DisabledProduct).filter( DisabledProduct.product == product).filter( DisabledProduct.branch == branch).one() session.delete(dp) session.commit() return 200 except NoResultFound: abort(404)
def add_release(body): # we must require scope which depends on XPI type xpi_type = _xpi_type(body["revision"], body["xpi_name"]) required_permission = f"{SCOPE_PREFIX}/add_release/xpi/{xpi_type}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort(401, f"required permission: {required_permission}, user permissions: {user_permissions}") session = current_app.db.session xpi = XPI(name=body["xpi_name"], revision=body["xpi_revision"], version=body["xpi_version"]) # project name used in the TC routes and matches the repo name project = current_app.config.get("XPI_MANIFEST_REPO") release = XPIRelease(revision=body["revision"], xpi=xpi, build_number=body["build_number"], status="scheduled", xpi_type=xpi_type, project=project) try: release.generate_phases() session.add(release) session.commit() except UnsupportedFlavor as e: raise BadRequest(description=e.description) logger.info("New release of %s", release.name) return release.json, 201
def disable_product(body): product = body["product"] branch = body["branch"] required_permission = f"{SCOPE_PREFIX}/disable_product/{product}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) session = current_app.db.session dp = DisabledProduct(product=product, branch=branch) session.add(dp) session.commit() logger.info("Disabled %s on branch %s", product, branch) notify_via_matrix( product, f"Automatic releases disabled for {product} on {branch}") return 200
def schedule_phase(name, phase): session = current_app.db.session phases = session.query(XPIPhase).filter( XPIRelease.id == XPIPhase.release_id).filter( XPIRelease.name == name).all() phase_to_schedule = list( filter(lambda _phase: _phase.name == phase, phases)) # Get email for all signoffs from previous phases and phase scheduler additional_shipit_emails = get_signoff_emails(phases) if not phase_to_schedule: abort(404, f"phase {phase} not found") phase_to_schedule = phase_to_schedule[0] # we must require scope which depends on XPI type xpi_type = _xpi_type(phase_to_schedule.release.revision, phase_to_schedule.release.xpi_name) required_permission = f"{SCOPE_PREFIX}/schedule_phase/xpi/{xpi_type}/{phase_to_schedule.name}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) scheduled_phase = do_schedule_phase(session, phase_to_schedule, additional_shipit_emails) url = taskcluster_urls.ui(get_root_url(), f"/tasks/groups/{scheduled_phase.task_id}") logger.info("Phase %s of %s started by %s. - %s", scheduled_phase.name, scheduled_phase.release.name, scheduled_phase.completed_by, url) notify_via_matrix( "xpi", f"Phase {scheduled_phase.name} of {scheduled_phase.release.name} started by {scheduled_phase.completed_by}. - {url}" ) return scheduled_phase.json
def phase_signoff(name, phase, body): session = current_app.db.session signoff = session.query(XPISignoff).filter(XPISignoff.uid == body).first_or_404() if signoff.signed: abort(409, "Already signed off") phase_obj = ( session.query(XPIPhase).filter(XPIRelease.id == XPIPhase.release_id).filter(XPIRelease.name == name).filter(XPIPhase.name == phase).first_or_404() ) # we must require scope which depends on product and phase name xpi_type = _xpi_type(phase_obj.release.revision, phase_obj.release.xpi_name) required_permission = f"{SCOPE_PREFIX}/phase_signoff/xpi/{xpi_type}/{signoff.phase.name}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort(401, f"required permission: {required_permission}, user permissions: {user_permissions}") # Prevent the same user signing off for multiple signoffs who = current_user.get_id() if who in [s.completed_by for s in phase_obj.signoffs]: abort(409, f"Already signed off by {who}") signoff.completed = datetime.datetime.utcnow() signoff.signed = True signoff.completed_by = who session.commit() signoffs = [s.json for s in phase_obj.signoffs] # Schedule the phase when all signoffs are done if all([s.signed for s in phase_obj.signoffs]): schedule_phase(name, phase) release = phase_obj.release logger.info("Phase %s of %s signed off by %s", phase, release.name, who) return dict(signoffs=signoffs)
def schedule_phase(name, phase): session = current_app.db.session phase = session.query(Phase).filter(Release.id == Phase.release_id).filter( Release.name == name).filter(Phase.name == phase).first_or_404() # we must require scope which depends on product required_permission = f"{SCOPE_PREFIX}/schedule_phase/{phase.release.product}/{phase.name}" if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) phase = do_schedule_phase(session, phase) url = taskcluster_urls.ui(get_root_url(), f"/tasks/groups/{phase.task_id}") logger.info("Phase %s of %s started by %s. - %s", phase.name, phase.release.name, phase.completed_by, url) notify_via_matrix( phase.release.product, f"Phase {phase.name} was just scheduled for {phase.release.name} - {url}" ) return phase.json
def add_release(body): # we must require scope which depends on product required_permission = f'{SCOPE_PREFIX}/add_release/{body["product"]}' if not current_user.has_permissions(required_permission): user_permissions = ", ".join(current_user.get_permissions()) abort( 401, f"required permission: {required_permission}, user permissions: {user_permissions}" ) product = body["product"] branch = body["branch"] product_disabled = branch in get_disabled_products().get(product, []) if current_user.type == AuthType.TASKCLUSTER and product_disabled: abort(401, "Taskcluster based submissions are disabled") session = current_app.db.session partial_updates = body.get("partial_updates") if partial_updates == "auto": if product not in [ Product.FIREFOX.value, Product.DEVEDITION.value, Product.PINEBUILD.value ] or branch not in [ "try", "releases/mozilla-beta", "projects/maple", ]: abort(400, "Partial suggestion works for automated betas only") partial_updates = _suggest_partials(product=product, branch=branch) release = Release( branch=branch, build_number=body["build_number"], partial_updates=partial_updates, product_key=body.get("product_key"), product=product, release_eta=body.get("release_eta"), repo_url=body.get("repo_url", ""), revision=body["revision"], status="scheduled", version=body["version"], ) try: next_version = bump_version(release.product, release.version) common_input = { "build_number": release.build_number, "next_version": next_version, # specify version rather than relying on in-tree version, # so if a version bump happens between the build and an action task # revision, we still use the correct version. "version": release.version, "release_eta": release.release_eta, "release_enable_emefree": is_eme_free_enabled(release.product, release.version), } partners_enabled = is_partner_enabled(release.product, release.version) if int(release.version.split(".")[0]) >= 81: common_input["release_enable_partner_repack"] = partners_enabled common_input[ "release_enable_partner_attribution"] = partners_enabled else: common_input["release_enable_partners"] = partners_enabled if release.partial_updates: common_input["partial_updates"] = release.partial_updates release.phases = generate_phases(release, common_input, verify_supported_flavors=True) session.add(release) session.commit() except UnsupportedFlavor as e: raise BadRequest(description=e.description) except (IntegrityError, TaskclusterRestFailure) as e: # Report back Taskcluster and SQL failures for better visibility of the # actual issue. Usually it happens when we cannot find the indexed # task or a duplicate release request accordingly. abort(400, str(e)) logger.info("New release of %s", release.name) notify_via_matrix(product, f"New release of {release.name}") return release.json, 201