Kaynağa Gözat

Add notifications for failed package updates, fixes #126

Jordi Boggiano 12 yıl önce
ebeveyn
işleme
f81d8bec4d

+ 16 - 0
app/Resources/FOSUserBundle/views/Profile/edit_content.html.twig

@@ -0,0 +1,16 @@
+<form action="{{ path('fos_user_profile_edit') }}" {{ form_enctype(form) }} method="POST" class="fos_user_profile_edit">
+    {{ form_row(form.username) }}
+    {{ form_row(form.email) }}
+    {{ form_row(form.current_password) }}
+
+    <div class="notifications">
+        {{ form_errors(form.failureNotifications) }}
+        {{ form_widget(form.failureNotifications) }}
+        {{ form_label(form.failureNotifications) }}
+    </div>
+
+    <div>
+        {{ form_widget(form) }}
+        <input type="submit" value="{{ 'profile.edit.submit'|trans({}, 'FOSUserBundle') }}" />
+    </div>
+</form>

+ 3 - 0
app/config/config.yml

@@ -75,6 +75,9 @@ fos_user:
     registration:
         form:
             handler: packagist.form.handler.registration
+    profile:
+        form:
+            type:       packagist_user_profile
 
 hwi_oauth:
     firewall_name: main

+ 16 - 2
src/Packagist/WebBundle/Command/UpdatePackagesCommand.php

@@ -22,8 +22,9 @@ use Composer\Repository\VcsRepository;
 use Composer\Factory;
 use Composer\Package\Loader\ValidatingArrayLoader;
 use Composer\Package\Loader\ArrayLoader;
-use Composer\IO\NullIO;
+use Composer\IO\BufferIO;
 use Composer\IO\ConsoleIO;
+use Composer\Repository\InvalidRepositoryException;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -40,6 +41,7 @@ class UpdatePackagesCommand extends ContainerAwareCommand
             ->setDefinition(array(
                 new InputOption('force', null, InputOption::VALUE_NONE, 'Force a re-crawl of all packages'),
                 new InputOption('delete-before', null, InputOption::VALUE_NONE, 'Force deletion of all versions before an update'),
+                new InputOption('notify-failures', null, InputOption::VALUE_NONE, 'Notify failures to maintainers by email'),
                 new InputArgument('package', InputArgument::OPTIONAL, 'Package name to update'),
             ))
             ->setDescription('Updates packages')
@@ -82,8 +84,12 @@ class UpdatePackagesCommand extends ContainerAwareCommand
         $updater = $this->getContainer()->get('packagist.package_updater');
         $start = new \DateTime();
 
+        if ($verbose && $input->getOption('notify-failures')) {
+            throw new \LogicException('Failures can not be notified in verbose mode since the output is piped to the CLI');
+        }
+
         $input->setInteractive(false);
-        $io = $verbose ? new ConsoleIO($input, $output, $this->getApplication()->getHelperSet()) : new NullIO;
+        $io = $verbose ? new ConsoleIO($input, $output, $this->getApplication()->getHelperSet()) : null;
         $config = Factory::createConfig();
         $loader = new ValidatingArrayLoader(new ArrayLoader());
 
@@ -95,9 +101,17 @@ class UpdatePackagesCommand extends ContainerAwareCommand
                     $output->writeln('Importing '.$package->getRepository());
                 }
                 try {
+                    if (null === $io || $io instanceof BufferIO) {
+                        $io = new BufferIO('');
+                    }
                     $repository = new VcsRepository(array('url' => $package->getRepository()), $io, $config);
                     $repository->setLoader($loader);
                     $updater->update($package, $repository, $flags, $start);
+                } catch (InvalidRepositoryException $e) {
+                    if ($input->getOption('notify-failures')) {
+                        $this->getContainer()->get('packagist.package_manager')->notifyUpdateFailure($package, $e, $io->getOutput());
+                    }
+                    $output->writeln('<error>Broken repository in '.$router->generate('view_package', array('name' => $package->getName()), true).': '.$e->getMessage().'</error>');
                 } catch (\Exception $e) {
                     $output->writeln('<error>Error updating '.$router->generate('view_package', array('name' => $package->getName()), true).' ['.get_class($e).']: '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().'</error>');
                 }

+ 4 - 5
src/Packagist/WebBundle/Controller/ApiController.php

@@ -15,6 +15,7 @@ namespace Packagist\WebBundle\Controller;
 use Composer\IO\BufferIO;
 use Composer\Factory;
 use Composer\Repository\VcsRepository;
+use Composer\Repository\InvalidRepositoryException;
 use Composer\Package\Loader\ValidatingArrayLoader;
 use Composer\Package\Loader\ArrayLoader;
 use Packagist\WebBundle\Package\Updater;
@@ -171,7 +172,6 @@ class ApiController extends Controller
         $updater = $this->get('packagist.package_updater');
         $em = $this->get('doctrine.orm.entity_manager');
 
-$candidate = array();
         foreach ($user->getPackages() as $package) {
             if (preg_match($urlRegex, $package->getRepository(), $candidate)
                 && $candidate['domain'] === $requestedRepo['domain']
@@ -187,11 +187,10 @@ $candidate = array();
                 $em->flush();
                 try {
                     $updater->update($package, $repository);
-                    if ($repository->hadInvalidBranches()) {
-                        throw new \RuntimeException('Some branches contained invalid data and were discarded, it is advised to review the log and fix any issues present in branches');
-                    }
                 } catch (\Exception $e) {
-                    // TODO send email to maintainer(s)
+                    if ($e instanceof InvalidRepositoryException) {
+                        $this->get('packagist.package_manager')->notifyUpdateFailure($package, $e, $io->getOutput());
+                    }
 
                     return new Response(json_encode(array(
                         'status' => 'error',

+ 0 - 3
src/Packagist/WebBundle/Controller/WebController.php

@@ -457,9 +457,6 @@ class WebController extends Controller
 
                 try {
                     $updater->update($package, $repository, Updater::UPDATE_TAGS);
-                    if ($repository->hadInvalidBranches()) {
-                        throw new \RuntimeException('Some branches contained invalid data and were discarded, it is advised to review the log and fix any issues present in branches');
-                    }
                 } catch (\Exception $e) {
                     return new Response(json_encode(array(
                         'status' => 'error',

+ 26 - 0
src/Packagist/WebBundle/Entity/Package.php

@@ -111,6 +111,11 @@ class Package
      */
     private $autoUpdated = false;
 
+    /**
+     * @ORM\Column(type="boolean", options={"default"=false})
+     */
+    private $updateFailureNotified = false;
+
     private $entityRepository;
 
     /**
@@ -371,6 +376,7 @@ class Package
     public function setUpdatedAt($updatedAt)
     {
         $this->updatedAt = $updatedAt;
+        $this->setUpdateFailureNotified(false);
     }
 
     /**
@@ -502,4 +508,24 @@ class Package
     {
         return $this->autoUpdated;
     }
+
+    /**
+     * Set updateFailureNotified
+     *
+     * @param Boolean $updateFailureNotified
+     */
+    public function setUpdateFailureNotified($updateFailureNotified)
+    {
+        $this->updateFailureNotified = $updateFailureNotified;
+    }
+
+    /**
+     * Get updateFailureNotified
+     *
+     * @return Boolean
+     */
+    public function isUpdateFailureNotified()
+    {
+        return $this->updateFailureNotified;
+    }
 }

+ 36 - 0
src/Packagist/WebBundle/Entity/User.php

@@ -62,6 +62,12 @@ class User extends BaseUser
      */
     private $githubToken;
 
+    /**
+     * @ORM\Column(type="boolean", options={"default"=true})
+     * @var string
+     */
+    private $failureNotifications = true;
+
     public function __construct()
     {
         $this->packages = new ArrayCollection();
@@ -197,4 +203,34 @@ class User extends BaseUser
     {
         $this->githubToken = $githubToken;
     }
+
+    /**
+     * Set failureNotifications
+     *
+     * @param Boolean $failureNotifications
+     */
+    public function setFailureNotifications($failureNotifications)
+    {
+        $this->failureNotifications = $failureNotifications;
+    }
+
+    /**
+     * Get failureNotifications
+     *
+     * @return Boolean
+     */
+    public function getFailureNotifications()
+    {
+        return $this->failureNotifications;
+    }
+
+    /**
+     * Get failureNotifications
+     *
+     * @return Boolean
+     */
+    public function isNotifiableForFailures()
+    {
+        return $this->failureNotifications;
+    }
 }

+ 34 - 0
src/Packagist/WebBundle/Form/Type/ProfileFormType.php

@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Form\Type;
+
+use FOS\UserBundle\Form\Type\ProfileFormType as BaseType;
+use Symfony\Component\Form\FormBuilderInterface;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class ProfileFormType extends BaseType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options)
+    {
+        parent::buildForm($builder, $options);
+
+        $builder->add('failureNotifications', null, array('required' => false, 'label' => 'Notify me of package update failures'));
+    }
+
+    public function getName()
+    {
+        return 'packagist_user_profile';
+    }
+}

+ 70 - 0
src/Packagist/WebBundle/Model/PackageManager.php

@@ -0,0 +1,70 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Model;
+
+use Swift_Mailer;
+use Twig_Environment;
+use Doctrine\ORM\EntityManager;
+use Packagist\WebBundle\Entity\Package;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class PackageManager
+{
+    protected $em;
+    protected $mailer;
+    protected $twig;
+    protected $options;
+
+    public function __construct(EntityManager $em, Swift_Mailer $mailer, Twig_Environment $twig, array $options)
+    {
+        $this->em = $em;
+        $this->mailer = $mailer;
+        $this->twig = $twig;
+        $this->options = $options;
+    }
+
+    public function notifyUpdateFailure(Package $package, \Exception $e, $details = null)
+    {
+        if (!$package->isUpdateFailureNotified()) {
+            $recipients = array();
+            foreach ($package->getMaintainers() as $maintainer) {
+                if ($maintainer->isNotifiableForFailures()) {
+                    $recipients[$maintainer->getEmail()] = $maintainer->getUsername();
+                }
+            }
+
+            if ($recipients) {
+                $body = $this->twig->render('PackagistWebBundle:Email:update_failed.txt.twig', array(
+                    'package' => $package,
+                    'exception' => get_class($e),
+                    'exceptionMessage' => $e->getMessage(),
+                    'details' => $details,
+                ));
+
+                $message = \Swift_Message::newInstance()
+                    ->setSubject($package->getName().' failed to update, invalid composer.json data')
+                    ->setFrom($this->options['from'], $this->options['fromName'])
+                    ->setTo($recipients)
+                    ->setBody($body)
+                ;
+
+                $this->mailer->send($message);
+            }
+
+            $package->setUpdateFailureNotified(true);
+            $this->em->flush();
+        }
+    }
+}

+ 5 - 0
src/Packagist/WebBundle/Package/Updater.php

@@ -15,6 +15,7 @@ namespace Packagist\WebBundle\Package;
 use Composer\Package\AliasPackage;
 use Composer\Package\PackageInterface;
 use Composer\Repository\RepositoryInterface;
+use Composer\Repository\InvalidRepositoryException;
 use Composer\Util\ErrorHandler;
 use Packagist\WebBundle\Entity\Author;
 use Packagist\WebBundle\Entity\Package;
@@ -97,6 +98,10 @@ class Updater
         $versions = $repository->getPackages();
         $em = $this->doctrine->getEntityManager();
 
+        if ($repository->hadInvalidBranches()) {
+            throw new InvalidRepositoryException('Some branches contained invalid data and were discarded, it is advised to review the log and fix any issues present in branches');
+        }
+
         usort($versions, function ($a, $b) {
             $aVersion = $a->getVersion();
             $bVersion = $b->getVersion();

+ 14 - 0
src/Packagist/WebBundle/Resources/config/services.yml

@@ -61,3 +61,17 @@ services:
             - @snc_redis.default_client
             - @packagist.package_repository
             - @packagist.user_repository
+
+    packagist.package_manager:
+        class: Packagist\WebBundle\Model\PackageManager
+        arguments:
+            - @doctrine.orm.entity_manager
+            - @mailer
+            - @twig
+            - { from: %mailer_from_email%, fromName: %mailer_from_name% }
+
+    packagist.profile.form.type:
+        class: Packagist\WebBundle\Form\Type\ProfileFormType
+        arguments: [%fos_user.model.user.class%]
+        tags:
+            - { name: form.type, alias: packagist_user_profile }

+ 21 - 0
src/Packagist/WebBundle/Resources/views/Email/update_failed.txt.twig

@@ -0,0 +1,21 @@
+> Disclaimer: These email notifications are in experimental stage, if you find
+> that they look strange or contain false or misleading information please
+> reply to this email to let us know.
+
+The {{ package.name }} package of which you are a maintainer has
+failed to update due to invalid data contained in your composer.json.
+Please address this as soon as possible since the package stopped updating.
+
+It is recommended that you use `composer validate` to check for errors when you
+change your composer.json.
+
+Below is the full update log which should highlight errors as
+"Skipped branch ...":
+
+[{{ exception }}]: {{ exceptionMessage }}
+
+{{ details }}
+
+--
+If you do not wish to receive such emails in the future you can disable
+notifications on your profile page: https://packagist.org/profile/edit