← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~alexsander-souza/maas/+git/maas-release-tools:improve_help into ~maas-committers/maas/+git/maas-release-tools:main

 

Alexsander de Souza has proposed merging ~alexsander-souza/maas/+git/maas-release-tools:improve_help into ~maas-committers/maas/+git/maas-release-tools:main.

Commit message:
Add fix instructions

Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~alexsander-souza/maas/+git/maas-release-tools/+merge/436480
-- 
Your team MAAS Committers is requested to review the proposed merge of ~alexsander-souza/maas/+git/maas-release-tools:improve_help into ~maas-committers/maas/+git/maas-release-tools:main.
diff --git a/maas_release_tools/git.py b/maas_release_tools/git.py
index 5e377c0..14420d7 100644
--- a/maas_release_tools/git.py
+++ b/maas_release_tools/git.py
@@ -69,6 +69,20 @@ class Git:
         output = self._run(*run_args).output
         return output.split("\n")
 
+    def get_commit_count(self, ref: str) -> str:
+        result = self._run("rev-list", "--count", ref)
+        return not result.succeeded
+
+    def list_changes(self) -> list[str]:
+        result = self._run("status", "-suno").output
+        return result.split("\n")
+
+    def get_user(self) -> str:
+        return self._run("config", "user.name").output
+
+    def get_email(self) -> str:
+        return self._run("config", "user.email").output
+
     def _run(self, *args) -> GitCommandResult:
         proc = subprocess.run(
             ["git", *args],
diff --git a/maas_release_tools/launchpad.py b/maas_release_tools/launchpad.py
index f070d9a..7ea3191 100644
--- a/maas_release_tools/launchpad.py
+++ b/maas_release_tools/launchpad.py
@@ -8,10 +8,13 @@ from pathlib import Path
 from typing import List, Optional, Sequence
 
 from launchpadlib.launchpad import Launchpad
+from lazr.restfulclient.errors import NotFound
 
 DONE_BUGS = ("Invalid", "Won't Fix", "Fix Committed", "Fix Released")
 UNFINISHED_BUGS = ("New", "Confirmed", "Triaged", "In Progress", "Incomplete")
 
+MAAS_USER = "maas-committers"
+
 
 class UnknownLaunchpadEntry(Exception):
     def __init__(self, entry_type: str, identifier: str):
@@ -42,6 +45,19 @@ class LaunchpadActions:
         """Return the logged in user user from LP."""
         return self.lp.me
 
+    @cached_property
+    def maas(self):
+        """Return the MAAS user from LP."""
+        return self.lp.people[MAAS_USER]
+
+    def snap_builder_exist(self, builder_name: str) -> bool:
+        try:
+            _ = self.lp.snaps.getByName(name=builder_name, owner=self.maas)
+        except NotFound:
+            return False
+        else:
+            return True
+
     def move_done_bugs(
         self,
         origin_milestone: str,
diff --git a/maas_release_tools/scripts/release_status.py b/maas_release_tools/scripts/release_status.py
index ca722d0..42ca459 100644
--- a/maas_release_tools/scripts/release_status.py
+++ b/maas_release_tools/scripts/release_status.py
@@ -18,8 +18,9 @@ import base64
 from functools import lru_cache
 import glob
 import json
+import os
 import sys
-from typing import Iterable, Optional
+from typing import Iterable, Optional, Tuple
 
 from debian.changelog import Changelog
 from lazr.restfulclient.errors import NotFound
@@ -28,7 +29,12 @@ import requests
 
 from . import convert_file_descriptors_to_path
 from ..git import Git
-from ..launchpad import DONE_BUGS, LaunchpadActions, UnknownLaunchpadEntry
+from ..launchpad import (
+    DONE_BUGS,
+    LaunchpadActions,
+    MAAS_USER,
+    UnknownLaunchpadEntry,
+)
 from ..maasci import (
     JenkinsActions,
     JenkinsConnectionFailed,
@@ -99,28 +105,40 @@ class ReleasePreparer:
         snapstore_auth,
         launchpad: LaunchpadActions,
         jenkins: JenkinsActions,
+        keep_going: bool,
     ):
         self.launchpad = launchpad
         self.jenkins = jenkins
         self.version = version
+        self.keep_going = keep_going
         self.snapstore_auth = snapstore_auth
         self.git_short_rev = Git().get_short_rev("HEAD")
 
     def run(self, args):
         all_good = True
         for step in self.steps:
+            how_to_fix = None
             if step.skip():
                 continue
             print(step.title, end=": ")
             success, message = step.check()
             if not success:
+                success, how_to_fix = step.fix()
+
+            if not success:
                 all_good = False
                 print("\N{Cross Mark}")
             else:
                 print("\N{White Heavy Check Mark}")
+
             if message:
                 for line in message.splitlines():
                     print("  " + str(line))
+            if how_to_fix:
+                for line in how_to_fix:
+                    print("   \N{Radio Button} " + f"{line}")
+            if not (all_good or self.keep_going):
+                break
 
         print()
         if all_good:
@@ -144,7 +162,7 @@ class ReleaseStep(ABC):
         return False
 
     @abstractmethod
-    def check(self):
+    def check(self) -> Tuple[bool, str]:
         """Return whether the step has already been performed.
 
         It returns a tuple of (succeeded, message), where result is a
@@ -152,58 +170,87 @@ class ReleaseStep(ABC):
         multi-line message to be displayed.
         """
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        """Return instructions to fix the issue.
+
+        When `doit` is True and this ReleaseStep can be fixed automatically, this
+        method should execute the procedure.
+
+        Returns a tuple of (fixed, how_to_fix), where fixed is True if the fix was
+        sucessful, and False when this failure could not be recovered. how_to_fix is
+        an array with manual steps the user should do to fix this.
+        """
+        return (
+            False,
+            [
+                "I don't know how to fix this \U0001F979",
+                "Please improve maas-release-tools",
+            ],
+        )
+
 
 class NoUncommittedChanges(ReleaseStep):
     @property
     def title(self):
         return "No uncommitted changes"
 
-    def check(self):
+    def check(self) -> Tuple[bool, str]:
         if self.git.has_uncommited_changes():
-            return False, "Commit and push all changes before releasing."
+            return False, "Your local repository is not clean"
         else:
             return True, None
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        return False, [
+            "Either commit or stash your changes:",
+            *self.git.list_changes(),
+        ]
+
 
 class CommitInRemoteBranch(ReleaseStep):
+    def __init__(self, preparer):
+        super().__init__(preparer)
+        self.release_branch_name = self.preparer.version.major
+        if self.preparer.version.grade in ("alpha", "beta"):
+            # alpha and beta releases are released from master
+            self.release_branch_name = "master"
+        self.official_maas_remote = get_official_maas_remote(self.git)
+
     @property
     def title(self):
         return "Release commit in remote branch"
 
-    def check(self):
-        release_branch_name = self.preparer.version.major
-        if self.preparer.version.grade in ("alpha", "beta"):
-            # alpha and beta releases are released from master
-            release_branch_name = "master"
-        official_maas_remote = get_official_maas_remote(self.git)
-        if not official_maas_remote:
+    def check(self) -> Tuple[bool, str]:
+        if not self.official_maas_remote:
             return False, "Official MAAS remote not found"
         remote_branches = self.git.get_remote_branches_containing("HEAD")
         for remote, branch_name in remote_branches:
             if (
-                remote == official_maas_remote
-                and branch_name == release_branch_name
+                remote == self.official_maas_remote
+                and branch_name == self.release_branch_name
             ):
                 return True, None
         else:
             error_message = (
                 "Current HEAD is not in "
-                f"{official_maas_remote}/{release_branch_name}"
+                f"{self.official_maas_remote}/{self.release_branch_name}"
             )
             return False, error_message
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        steps = [f"git checkout {self.release_branch_name}"]
+        return False, steps
+
 
 class MAASVersion(ReleaseStep):
     @property
     def title(self):
         return "MAAS version set in branch"
 
-    def check(self):
+    def check(self) -> Tuple[bool, str]:
         setup_version = get_branch_setup_version()
         if setup_version != self.preparer.version.python_version:
-            error_message = (
-                f"setup.cfg has {setup_version}, run 'release-prepare'"
-            )
+            error_message = f"setup.cfg has {setup_version} (expected {self.preparer.version.python_version})"
             return False, error_message
         deb_ver = self.preparer.version.deb_version
         with open("debian/changelog", "r") as fh:
@@ -215,6 +262,22 @@ class MAASVersion(ReleaseStep):
                 )
         return True, None
 
+    def fix(self, doit=False):
+        if self.preparer.version.grade == "beta":
+            branch = "master"
+        else:
+            branch = self.preparer.version.major
+        return False, [
+            "Go to http://maas-ci.internal:8080/job/maas-version-bump/build";,
+            f"Set 'LP_BRANCH' to '{branch}'",
+            f"Set 'RELEASE_VERSION' to '{self.preparer.version.python_version}'",
+            f"Set 'DEBFULLNAME' to '{self.git.get_user()}' *",
+            f"Set 'DEBEMAIL' to '{self.git.get_email()}' *",
+            "  * Your user and email should match your GPG key",
+            "Press 'Build'",
+            "run 'git pull --recurse-submodules' to update the local repository",
+        ]
+
 
 class SnapTrack(ReleaseStep):
     def __init__(self, preparer, snap_name):
@@ -243,6 +306,45 @@ class SnapTrack(ReleaseStep):
         auth_error = get_macaroon_auth_error(res, self.snap_name)
         return res.status_code == 200, auth_error
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        procedure = [
+            "Go to the snapstore discourse forum (https://forum.snapcraft.io/).",
+            "Post a message requesting a new track, use this template: https://forum.snapcraft.io/t/3-3-tracks-for-maas-and-maas-test-db/32141.";,
+            "Be *sure* to update the version number and associated regex, and set the tag to `store-requests`.",
+            "Pay attention to the forum for a response. It may take some time, in some cases.",
+        ]
+        return False, procedure
+
+
+class PPACopyMixin:
+    def check_packages_copied(self, source_ppa, target_ppa):
+        target_packages = list(
+            (package.source_package_name, package.source_package_version)
+            for package in target_ppa.getPublishedSources(
+                status="Published", distro_series=self.current_series
+            )
+        )
+        missing_packages = set()
+        for package in source_ppa.getPublishedSources(
+            status="Published", distro_series=self.current_series
+        ):
+            name, version = (
+                package.source_package_name,
+                package.source_package_version,
+            )
+            if (name, version) not in target_packages:
+                missing_packages.add((name, version))
+
+        if missing_packages:
+            error_message = "\n".join(
+                f"{name} {version} has not been copied"
+                for name, version in sorted(missing_packages)
+            )
+            # error_message += f"\nGo to {source_ppa.web_link}/+copy-packages"
+            return False, error_message
+        else:
+            return True, None
+
 
 class MAASPPA(ReleaseStep):
     def __init__(self, preparer, ppa_type):
@@ -261,56 +363,51 @@ class MAASPPA(ReleaseStep):
         self.current_series = ubuntu.getSeries(
             name_or_version=get_ubuntu_series()
         )
+        self.ppa = None
 
     @property
     def title(self):
         return f"MAAS {self.ppa_type} PPA ({self.ppa_path})"
 
-    def check(self):
+    def load_ppa(self):
         try:
-            ppa = self.ppa_owner.getPPAByName(name=self.ppa_name)
+            self.ppa = self.ppa_owner.getPPAByName(name=self.ppa_name)
         except NotFound:
+            return False
+        else:
+            return True
+
+    def check(self):
+        if not self.load_ppa():
             return (
                 False,
                 f"ppa:{self.ppa_owner.name}/{self.ppa_name} couldn't be found.",
             )
-        else:
-            ppa_archs = set(processor.name for processor in ppa.processors)
-            missing_archs = sorted(set(BUILD_ARCHS).difference(ppa_archs))
-            if missing_archs:
-                return False, (
-                    f"Missing build architectures: {', '.join(missing_archs)}"
-                )
 
-            return True, None
-
-    def _check_packages_copied(self, source_ppa, target_ppa):
-        target_packages = list(
-            (package.source_package_name, package.source_package_version)
-            for package in target_ppa.getPublishedSources(
-                status="Published", distro_series=self.current_series
-            )
-        )
-        missing_packages = set()
-        for package in source_ppa.getPublishedSources(
-            status="Published", distro_series=self.current_series
-        ):
-            name, version = (
-                package.source_package_name,
-                package.source_package_version,
+        ppa_archs = set(processor.name for processor in self.ppa.processors)
+        missing_archs = sorted(set(BUILD_ARCHS).difference(ppa_archs))
+        if missing_archs:
+            return False, (
+                f"Missing build architectures: {', '.join(missing_archs)}"
             )
-            if (name, version) not in target_packages:
-                missing_packages.add((name, version))
+        return True, None
 
-        if missing_packages:
-            error_message = "\n".join(
-                f"{name} {version} has not been copied"
-                for name, version in sorted(missing_packages)
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        steps = []
+        if self.ppa is None:
+            steps.append(
+                f"Go to https://launchpad.net/~{self.ppa_owner.name}/+activate-ppa";
             )
-            error_message += f"\nGo to {source_ppa.web_link}/+copy-packages"
-            return False, error_message
+            steps.append(f"Create '{self.ppa_name}' PPA")
         else:
-            return True, None
+            steps.append(
+                f"Go to https://launchpad.net/~{self.ppa_owner.name}/+archive/ubuntu/{self.ppa_name}/";
+            )
+        steps.append("Click `Change Details`")
+        steps.append(
+            f"Enable the following processors: {', '.join(sorted(set(BUILD_ARCHS)))}"
+        )
+        return False, steps
 
 
 class MAASPackagePublished(MAASPPA):
@@ -322,56 +419,54 @@ class MAASPackagePublished(MAASPPA):
         return f"MAAS package published in ({self.ppa_path})"
 
     def check(self):
-        try:
-            ppa = self.ppa_owner.getPPAByName(name=self.ppa_name)
-        except NotFound:
+        if not self.load_ppa():
             return (
                 False,
                 f"ppa:{self.ppa_path} couldn't be found.",
             )
-        else:
-            sources = list(
-                ppa.getPublishedSources(
-                    source_name="maas",
-                    status="Published",
-                    distro_series=self.current_series,
-                )
+
+        sources = list(
+            self.ppa.getPublishedSources(
+                source_name="maas",
+                status="Published",
+                distro_series=self.current_series,
             )
-            if not sources:
-                return False, (
-                    "Source package hasn't been published or uploaded yet."
-                )
-            [package] = sources
-            if not self._check_version(package.source_package_version):
-                expected = self.preparer.version.deb_version
-                return False, (
-                    f"Currently published source version is {package.source_package_version}. Expected {expected}"
-                )
-            binaries = list(
-                ppa.getPublishedBinaries(
-                    binary_name="maas",
-                    exact_match=True,
-                    status="Published",
-                )
+        )
+        if not sources:
+            return False, (
+                "Source package hasn't been published or uploaded yet."
             )
-            if not binaries:
-                return False, "Binary packages haven't been published yet."
-            published_architectures = set()
-            for binary in binaries:
-                arch = binary.distro_arch_series_link.split("/")[-1]
-                if self._check_version(binary.binary_package_version):
-                    published_architectures.add(arch)
-
-            non_published_architectures = sorted(
-                set(BUILD_ARCHS).difference(published_architectures)
+        [package] = sources
+        if not self._check_version(package.source_package_version):
+            expected = self.preparer.version.deb_version
+            return False, (
+                f"Currently published source version is {package.source_package_version}. Expected {expected}"
+            )
+        binaries = list(
+            self.ppa.getPublishedBinaries(
+                binary_name="maas",
+                exact_match=True,
+                status="Published",
+            )
+        )
+        if not binaries:
+            return False, "Binary packages haven't been published yet."
+        published_architectures = set()
+        for binary in binaries:
+            arch = binary.distro_arch_series_link.split("/")[-1]
+            if self._check_version(binary.binary_package_version):
+                published_architectures.add(arch)
+
+        non_published_architectures = sorted(
+            set(BUILD_ARCHS).difference(published_architectures)
+        )
+        if non_published_architectures:
+            return False, (
+                "Binary package hasn't been published for: "
+                f"{non_published_architectures}"
             )
-            if non_published_architectures:
-                return False, (
-                    "Binary package hasn't been published for: "
-                    f"{non_published_architectures}"
-                )
 
-            return True, None
+        return True, None
 
     def _check_version(self, package_version):
         expected_package_version = self.preparer.version.deb_version
@@ -383,41 +478,53 @@ class MAASPackagePublished(MAASPPA):
             and version_parts[2] == f"g.{self.preparer.git_short_rev}"
         )
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        if self.ppa is None:
+            return super().fix(doit)
+        return False, [
+            f"Check https://launchpad.net/~/+archive/ubuntu/{self.ppa_name}/+packages";,
+        ]
+
 
-class PackagesCopiedFromDeps(MAASPPA):
+class PackagesCopiedFromDeps(MAASPPA, PPACopyMixin):
     def __init__(self, preparer):
         super().__init__(preparer, "release-preparation")
+        self.source_ppa = None
+        self.target_ppa = None
 
     @property
     def title(self):
-        return "Packages copied from ppa:maas-committers/latest-deps"
+        return f"Packages copied from ppa:{MAAS_USER}/latest-deps"
 
     def check(self):
         try:
-            source_ppa = self.preparer.launchpad.lp.people[
-                "maas-committers"
-            ].getPPAByName(name="latest-deps")
+            self.source_ppa = self.preparer.launchpad.maas.getPPAByName(
+                name="latest-deps"
+            )
         except NotFound:
-            return False, "ppa:maas-committers/latest-deps couldn't be found."
+            return False, f"ppa:{MAAS_USER}/latest-deps couldn't be found."
         try:
-            target_ppa = self.ppa_owner.getPPAByName(name=self.ppa_name)
+            self.target_ppa = self.ppa_owner.getPPAByName(name=self.ppa_name)
         except NotFound:
             return (
                 False,
                 f"ppa:{self.ppa_path} couldn't be found.",
             )
         else:
-            return self._check_packages_copied(source_ppa, target_ppa)
+            return self.check_packages_copied(self.source_ppa, self.target_ppa)
 
+    # def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
 
-class PackagesCopiedToReleasePPA(MAASPPA):
+
+class PackagesCopiedToReleasePPA(MAASPPA, PPACopyMixin):
     @property
     def title(self):
         return f"Packages copied to ppa:{self.ppa_path}"
 
     def skip(self):
         return (
-            self.preparer.version.grade == "beta" and self.ppa_type == "stable"
+            self.preparer.version.grade != "stable"
+            and self.ppa_type == "stable"
         )
 
     def check(self):
@@ -441,7 +548,7 @@ class PackagesCopiedToReleasePPA(MAASPPA):
                 f"ppa:{self.ppa_path} couldn't be found.",
             )
         else:
-            return self._check_packages_copied(source_ppa, target_ppa)
+            return self.check_packages_copied(source_ppa, target_ppa)
 
 
 def macaroon_auth(macaroons):
@@ -472,11 +579,19 @@ class PackageBuilt(ReleaseStep):
         if len(tar_gzs) == 0:
             return False, (
                 "No orig.tar.gz could be found for the current revision.\n"
-                "Run release-build."
             )
         [orig_tgz] = tar_gzs
         return True, None
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        return False, [
+            f"export DEBFULLNAME='{self.git.get_user()}'",
+            f"export DEBEMAIL='{self.git.get_email()}'",
+            "make install-dependencies",
+            f"{os.path.dirname(sys.argv[0])}/release-build {get_ubuntu_series()}",
+            f"dput ppa:{self.preparer.launchpad.me.name}/maas-{self.preparer.version.major}-next build_pkg/maas_{self.preparer.version.deb_version}-*_source.changes",
+        ]
+
 
 class SnapsUploaded(ReleaseStep):
 
@@ -486,6 +601,7 @@ class SnapsUploaded(ReleaseStep):
     def title(self):
         return "Snaps have been built and uploaded to the store."
 
+    @lru_cache(maxsize=1)
     def _get_revisisions(self):
         # XXX: This considers only the last 500 uploaded revisions. That's fine
         #     if you're currently working on the release, but it will
@@ -536,6 +652,28 @@ class SnapsUploaded(ReleaseStep):
 
         return True, "\n".join(revision_info)
 
+    def fix(self, doit=False):
+        builder = f"maas-{self.preparer.version.major}"
+        steps = []
+        if self.preparer.launchpad.snap_builder_exist(builder):
+            steps.append(
+                f"check the snap package status at https://launchpad.net/~{MAAS_USER}/+snap/{builder}";
+            )
+        else:
+            steps.extend(
+                [
+                    f" go to https://code.launchpad.net/~{MAAS_USER}/maas/+git/maas/+ref/{self.preparer.version.major}/+new-snap";,
+                    f"  * snap recipe name: maas-{self.preparer.version.major}",
+                    "  * owner: maas",
+                    f"  * processors: {', '.join(BUILD_ARCHS)}",
+                    "  * Automatically build when branch changes",
+                    f"   * Source archive for automatic builds: ~maas/ubuntu/{self.preparer.version.major}-next",
+                    "  *  Automatically upload to store",
+                    f"   * Track: {self.preparer.version.major} Risk: Edge",
+                ]
+            )
+        return False, steps
+
 
 class SnapsInChannel(SnapsUploaded):
 
@@ -544,6 +682,7 @@ class SnapsInChannel(SnapsUploaded):
     def __init__(self, preparer, channel):
         super().__init__(preparer)
         self.channel = channel
+        self.missing_archs = []
 
     @property
     def title(self):
@@ -561,11 +700,32 @@ class SnapsInChannel(SnapsUploaded):
                         released_archs.add(arch)
                         break
 
-        missing_archs = sorted(set(BUILD_ARCHS).difference(released_archs))
-        if missing_archs:
-            return False, (f"Missing releases for: {', '.join(missing_archs)}")
+        self.missing_archs = sorted(
+            set(BUILD_ARCHS).difference(released_archs)
+        )
+        if self.missing_archs:
+            return False, (
+                f"Missing releases for: {', '.join(self.missing_archs)}"
+            )
         return True, None
 
+    def fix(self, doit=False):
+        steps = []
+        revision_map, _ = self._get_revisisions()
+
+        for arch in self.missing_archs:
+            if revision_map is None or len(revision_map.get(arch, [])) == 0:
+                steps.append(f"Release for {arch} not built yet, check above.")
+            else:
+                latest_revision = max(
+                    revision["revision"] for revision in revision_map[arch]
+                )
+                steps.append(
+                    f"snapcraft release maas {latest_revision} {self.channel}"
+                )
+
+        return False, steps
+
 
 class ReleaseTagged(ReleaseStep):
     @property
@@ -598,6 +758,12 @@ class ReleaseTagged(ReleaseStep):
 
         return True, None
 
+    def fix(self, doit=False):
+        return False, [
+            f"git tag {self.preparer.version.version}",
+            "git push --tags",
+        ]
+
 
 class VersionBranch(ReleaseStep):
     def __init__(
@@ -617,7 +783,7 @@ class VersionBranch(ReleaseStep):
     @property
     def _branch_version(self) -> str:
         drop_idx = self.preparer.version.version.rfind(".")
-        return self.preparer.version.version[:drop_idx]
+        return str(self.preparer.version.version[:drop_idx])
 
     @property
     def _ref_version(self) -> str:
@@ -654,21 +820,32 @@ class MilestoneExist(ReleaseStep):
         else:
             return True, None
 
+    def fix(self, doit: bool = False) -> Tuple[bool, Optional[list[str]]]:
+        return False, [
+            f"go to https://launchpad.net/maas/{self.preparer.version.major}/+addmilestone";,
+            f"Create {self.preparer.version.version} milestone",
+        ]
+
 
 class BugMovedToMilestone(ReleaseStep):
+    def __init__(self, preparer):
+        super().__init__(preparer)
+        self._ms_found = False
+
     @property
     def title(self):
         return "Bugs moved to Milestone on Launchpad"
 
     def check(self):
-        tag_name = self.preparer.version.version
+        tag_name = self.preparer.version.final_version
         try:
             ms = self.preparer.launchpad._get_milestone(tag_name)
+            self._ms_found = True
             bug_tasks = ms.searchTasks(status=DONE_BUGS)
-            if len(bug_tasks) == 0:
+            if len(bug_tasks) > 0:
                 return (
                     False,
-                    f"Bugs not copied to milestone {tag_name}, use 'release-manage move-done-bugs' to fix this",
+                    "Bugs not copied to milestone.",
                 )
         except UnknownLaunchpadEntry:
             return (
@@ -678,8 +855,19 @@ class BugMovedToMilestone(ReleaseStep):
         else:
             return True, None
 
+    def fix(self, doit: bool = False) -> Tuple[bool, Optional[list[str]]]:
+        if not self._ms_found:
+            return False, ["Pre-requesites check has failed, check above"]
+        return False, [
+            f"run {os.path.dirname(sys.argv[0])}/release-manage maas move-done-bugs {self.preparer.version.final_version} {self.preparer.version.version}"
+        ]
+
 
 class MilestoneReleased(ReleaseStep):
+    def __init__(self, preparer):
+        super().__init__(preparer)
+        self._ms_found = False
+
     @property
     def title(self):
         return "Milestone released on Launchpad"
@@ -688,10 +876,11 @@ class MilestoneReleased(ReleaseStep):
         tag_name = self.preparer.version.version
         try:
             ms = self.preparer.launchpad._get_milestone(tag_name)
-            if ms.is_active or len(ms.searchTasks(status="Fix Committed")) > 0:
+            self._ms_found = True
+            if ms.is_active:
                 return (
                     False,
-                    "Milestone not released, use 'release-manage release-milestone' to fix this",
+                    "Milestone not released",
                 )
         except UnknownLaunchpadEntry:
             return (
@@ -701,6 +890,13 @@ class MilestoneReleased(ReleaseStep):
         else:
             return True, None
 
+    def fix(self, doit: bool = False) -> Tuple[bool, Optional[list[str]]]:
+        if not self._ms_found:
+            return False, ["Pre-requesites check has failed, check above"]
+        return False, [
+            f"run {os.path.dirname(sys.argv[0])}/release-manage maas release-milestone {self.preparer.version.version}'"
+        ]
+
 
 class SystemIntegrationTests(ReleaseStep):
     def __init__(
@@ -710,6 +906,7 @@ class SystemIntegrationTests(ReleaseStep):
     ):
         super().__init__(preparer)
         self._job = job_name
+        self._url = None
 
     @property
     def title(self):
@@ -717,13 +914,16 @@ class SystemIntegrationTests(ReleaseStep):
 
     def check(self):
         try:
-            result, url = self.preparer.jenkins.get_last_build_result_for_rev(
+            (
+                result,
+                self._url,
+            ) = self.preparer.jenkins.get_last_build_result_for_rev(
                 self._job, self.preparer.git_short_rev
             )
             if result == JJB_FAILURE:
                 return (
                     False,
-                    f"Last build has failed, check {url}",
+                    "Last build has failed (this is not a fatal error)",
                 )
         except JenkinsConnectionFailed:
             return (
@@ -733,11 +933,21 @@ class SystemIntegrationTests(ReleaseStep):
         else:
             return True, None
 
+    def fix(self, doit: bool = False) -> Tuple[bool, list[str]]:
+        # Not fatal
+        return True, [f"check {self._url}"]
+
 
 def parse_args():
     parser = ArgumentParser(description=__doc__)
     parser.add_argument("version", help="The version of MAAS to be released")
     parser.add_argument(
+        "--keep-going",
+        action="store_true",
+        dest="keep_going",
+        help="Don't stop on first error",
+    )
+    parser.add_argument(
         "--dry-run",
         action="store_true",
         dest="dry_run",
@@ -793,6 +1003,7 @@ def main():
         macaroon_auth(macaroons),
         launchpad=launchpad,
         jenkins=jenkins,
+        keep_going=args.keep_going,
     )
     preparer.steps = [
         MAASVersion(preparer),
@@ -805,7 +1016,7 @@ def main():
         ),
         VersionBranch(
             preparer,
-            f"git+ssh://{launchpad.lp.me.name}@git.launchpad.net/~maas-committers/maas/+git/maas-test-db",
+            f"git+ssh://{launchpad.lp.me.name}@git.launchpad.net/~{MAAS_USER}/maas/+git/maas-test-db",
         ),
         SnapTrack(preparer, "maas"),
         SnapTrack(preparer, "maas-test-db"),
diff --git a/maas_release_tools/version.py b/maas_release_tools/version.py
index deb29bf..cd5fbca 100644
--- a/maas_release_tools/version.py
+++ b/maas_release_tools/version.py
@@ -29,6 +29,7 @@ class ReleaseVersion:
         self.grade = self._grade()
         self.snap_channels = self._snap_channels()
         self.deb_version = self.version.replace("-", "~")
+        self.final_version = self._final()
 
     def _python_version(self) -> Version:
         string_version = (
@@ -48,6 +49,11 @@ class ReleaseVersion:
         else:
             raise InvalidReleaseVersion(f"Unknown version suffix: {suffix}")
 
+    def _final(self) -> str:
+        if "-" not in self.version:
+            return self.version
+        return self.version.split("-")[0]
+
     def _snap_channels(self) -> Iterable[str]:
         grade_map = {
             "final": "stable",

Follow ups