Repository: SciTools/iris (language: python)

Issue:
Updated citation
## 🚀 Pull Request

### Description
A documentation tweak to update the citation to a more recent version (it's currently using version 1.2) and to better use DOIs.

Implements #4679



Discussion / hints:

Fix patch:
diff --git a/docs/src/developers_guide/release.rst b/docs/src/developers_guide/release.rst
index 1e18677808..97d7918148 100644
--- a/docs/src/developers_guide/release.rst
+++ b/docs/src/developers_guide/release.rst
@@ -7,47 +7,112 @@ Releases
 
 A release of Iris is a `tag on the SciTools/Iris`_ Github repository.
 
-The summary below is of the main areas that constitute the release.  The final
-section details the :ref:`iris_development_releases_steps` to take.
+Below is :ref:`iris_development_releases_steps`, followed by some prose on the
+main areas that constitute the release.
+
+
+.. _iris_development_releases_steps:
+
+How to Create an Iris Release
+-----------------------------
+
+The step-by-step process is walked-through by a script at:
+``<Iris repo root>/tools/release_do_nothing.py``, and also available here:
+:doc:`release_do_nothing`.
 
 
 .. _release_manager:
 
 Release Manager
 ---------------
+
 A Release Manager will be nominated for each release of Iris. This role involves:
 
 * deciding which features and bug fixes should be included in the release
-* managing the project board for the release
+* managing the `GitHub Projects`_ board for the release
 * using :discussion:`GitHub Discussion releases category <categories/releases>`
   for documenting intent and capturing any
   discussion about the release
+* holding a developer retrospective post release, to look for potential
+  future improvements
 
 The Release Manager will make the release, ensuring that all the steps outlined
 on this page are completed.
 
 
+Versioning
+----------
+
+Iris' version numbers conform to `Semantic Versioning`_ (``MAJOR.MINOR.PATCH``)
+and `PEP 440`_.
+
+Iris uses `setuptools-scm`_ to automatically manage versioning based on Git
+tags. No manual versioning work is required within the files themselves.
+
+
+Release Candidate
+-----------------
+
+Prior to a release, a release candidate tag may be created, marked as a
+pre-release in GitHub, with a tag ending with :literal:`rc` followed by a
+number (0-based), e.g.,:
+
+    :literal:`v1.9.0rc0`
+
+If created, the pre-release shall be available for a minimum of 2 weeks
+prior to the release being cut.  However a 4 week period should be the goal
+to allow user groups to be notified of the existence of the pre-release and
+encouraged to test the functionality.
+
+A pre-release is expected for a major or minor release, but not for a
+patch release.
+
+If new features are required for a release after a release candidate has been
+cut, a new pre-release shall be issued first.
+
+Release candidates are made available as a conda package on the
+`conda-forge Anaconda channel`_ using the `rc_iris`_ label. This is achieved via
+the `conda-forge iris-feedstock`_ following `CFEP-05`_. For further information
+see the `conda-forge User Documentation`_.
+
+
+Patch Releases
+--------------
+
+Patch releases may be implemented to fix problems with previous major or minor
+releases. E.g. ``v1.9.1`` to fix a problem in ``v1.9.0``, both being part of
+the ``v1.9`` series.
+
+New features shall not be included in a patch release, these are for bug fixes.
+
+A patch release does not require a release candidate, but the rest of the
+release process is to be followed.
+
+
 Before Release
 --------------
 
 Deprecations
 ~~~~~~~~~~~~
 
-Ensure that any behaviour which has been deprecated for the correct number of
+Any behaviour which has been deprecated for the correct number of
 previous releases is now finally changed. More detail, including the correct
 number of releases, is in :ref:`iris_development_deprecations`.
 
 Standard Names
 ~~~~~~~~~~~~~~
 
-Update the file ``etc/cf-standard-name-table.xml`` to the latest CF standard names,
+The file ``etc/cf-standard-name-table.xml`` is updated to the latest CF standard names,
 from the `latest CF standard names`_.
 ( This is used during build to automatically generate the sourcefile
 ``lib/iris/std_names.py``. )
 
 
+The Release
+-----------
+
 Release Branch
---------------
+~~~~~~~~~~~~~~
 
 Once the features intended for the release are on ``main``, a release branch
 should be created, in the ``SciTools/iris`` repository.  This will have the name:
@@ -61,73 +126,93 @@ for example:
 This branch shall be used to finalise the release details in preparation for
 the release candidate.
 
+Changes for a **patch release** should target to the same release branch as the
+rest of the series. For example, a fix
+for a problem with the ``v1.9.0`` release will be merged into ``v1.9.x`` release
+branch, and then released with the tag ``v1.9.1``.
 
-Release Candidate
------------------
+Documentation
+~~~~~~~~~~~~~
 
-Prior to a release, a release candidate tag may be created, marked as a
-pre-release in GitHub, with a tag ending with :literal:`rc` followed by a
-number (0-based), e.g.,:
+The documentation should include a dedicated What's New file for this release
+series (e.g. ``v1.9.rst``), incorporating all of the What's New entries for the release.
+This content should be reviewed and adapted as required, including highlights
+at the top of the What's New document.
 
-    :literal:`v1.9.0rc0`
+What's New entries for **patch releases** should be added to the existing file
+for that release series (e.g. ``v1.9.1`` section in the ``v1.9.rst`` file).
 
-If created, the pre-release shall be available for a minimum of two weeks
-prior to the release being cut.  However a 4 week period should be the goal
-to allow user groups to be notified of the existence of the pre-release and
-encouraged to test the functionality.
+A template for What's New formatting can be found in the
+``docs/src/whatsnew/latest.rst.template`` file.
 
-A pre-release is expected for a major or minor release, but not for a
-point release.
 
-If new features are required for a release after a release candidate has been
-cut, a new pre-release shall be issued first.
+Tagging
+~~~~~~~
 
-Make the release candidate available as a conda package on the
-`conda-forge Anaconda channel`_ using the `rc_iris`_ label. To do this visit
-the `conda-forge iris-feedstock`_ and follow `CFEP-05`_. For further information
-see the `conda-forge User Documentation`_.
+Once all checks are complete, the release is published from the release
+branch - via the GitHub release functionality in the ``SciTools/iris``
+repository - which simultaneously creates a Git tag for the release.
 
 
-Documentation
--------------
+Post Release
+------------
 
-The documentation should include all of the ``whatsnew`` entries for the release.
-This content should be reviewed and adapted as required.
+PyPI
+~~~~
+Iris is available on PyPI as ``scitools-iris``.
 
-Steps to achieve this can be found in the :ref:`iris_development_releases_steps`.
+Iris' Continuous-Integration (CI) includes the automatic building and publishing of
+PyPI artifacts in a dedicated GitHub Action.
 
+Legacy manual instructions are appended to this page for reference purposes
+(:ref:`update_pypi`)
 
-The Release
------------
+conda-forge
+~~~~~~~~~~~
+
+Iris is available on conda-forge as ``iris``.
 
-The final steps of the release are to ensure that the release date and details
-are correct in the relevant ``whatsnew`` page within the documentation.
+This is managed via the the Iris conda recipe on the
+`conda-forge iris-feedstock`_, which is updated after the release is cut on
+GitHub, followed by automatic build and publish of the
+conda package on the `conda-forge Anaconda channel`_.
 
-There is no need to update the ``iris.__version__``, as this is managed
-automatically by `setuptools-scm`_.
+Announcement
+~~~~~~~~~~~~
 
-Once all checks are complete, the release is published on GitHub by
-creating a new tag in the ``SciTools/iris`` repository.
+Iris uses Twitter (`@scitools_iris`_) to announce new releases, as well as any
+internal message boards that are accessible (e.g. at the UK Met Office).
+Announcements usually include a highlighted feature to hook readers' attention.
 
+Citation
+~~~~~~~~
 
-Update conda-forge
-------------------
+``docs/src/userguide/citation.rst`` is updated to include
+the latest [non-release-candidate] version, date and `Zenodo DOI`_
+of the new release. Ideally this would be updated before the release, but
+the DOI for the new version is only available once the release has been
+created in GitHub.
+
+Merge Back
+~~~~~~~~~~
+
+After any release is published, **including patch releases**, the changes from the
+release branch should be merged back onto the ``SciTools/iris`` ``main`` branch.
 
-Once a release is cut on GitHub, update the Iris conda recipe on the
-`conda-forge iris-feedstock`_ for the release. This will build and publish the
-conda package on the `conda-forge Anaconda channel`_.
 
+Appendices
+----------
 
 .. _update_pypi:
 
-Update PyPI
------------
+Updating PyPI Manually
+~~~~~~~~~~~~~~~~~~~~~~
 
 .. note::
 
   As part of our Continuous-Integration (CI), the building and publishing of
   PyPI artifacts is now automated by a dedicated GitHub Action.
-  
+
   The following instructions **no longer** require to be performed manually,
   but remain part of the documentation for reference purposes only.
 
@@ -145,7 +230,7 @@ Checkout the appropriate Iris ``<release>`` tag from the appropriate ``<repo>``.
 For example, to checkout tag ``v1.0`` from ``upstream``::
 
     > git fetch upstream --tags
-    > git checkout v1.0 
+    > git checkout v1.0
 
 Build the source distribution and wheel from the Iris root directory::
 
@@ -184,105 +269,6 @@ For further details on how to test Iris, see :ref:`developer_running_tests`.
     For further information on packaging and uploading a project to PyPI, please
     refer to `Generating Distribution Archives`_ and `Packaging Your Project`_.
 
-
-Merge Back
-----------
-
-After the release is published, the changes from the release branch should be merged
-back onto the ``SciTools/iris`` ``main`` branch.
-
-To achieve this, first cut a local branch from the latest ``main`` branch,
-and `git merge` the :literal:`.x` release branch into it. Ensure that the
-``docs/src/whatsnew/index.rst`` and ``docs/src/whatsnew/latest.rst`` are
-correct, before committing these changes and then proposing a pull-request
-on the ``main`` branch of ``SciTools/iris``.
-
-
-Point Releases
---------------
-
-Bug fixes may be implemented and targeted on the :literal:`.x` release branch.
-These should lead to a new point release, and another tag.  For example, a fix
-for a problem with the ``v1.9.0`` release will be merged into ``v1.9.x`` release
-branch, and then released by tagging ``v1.9.1``.
-
-New features shall not be included in a point release, these are for bug fixes.
-
-``whatsnew`` entries should be added to the existing 
-``docs/src/whatsnew/v1.9.rst`` file in a new ``v1.9.1`` section. A template for 
-this bugfix patches section can be found in the 
-``docs/src/whatsnew/latest.rst.template`` file.
-
-A point release does not require a release candidate, but the rest of the
-release process is to be followed, including the merge back of changes into
-``main``.
-
-
-.. _iris_development_releases_steps:
-
-Maintainer Steps
-----------------
-
-These steps assume a release for ``1.9.0`` is to be created.
-
-Release Steps
-~~~~~~~~~~~~~
-
-#. Update the ``whatsnew`` for the release:
-
-   * Use ``git`` to rename ``docs/src/whatsnew/latest.rst`` to the release
-     version file ``v1.9.rst``
-   * Use ``git`` to delete the ``docs/src/whatsnew/latest.rst.template`` file
-   * In ``v1.9.rst`` remove the ``[unreleased]`` caption from the page title.
-     Replace this with ``[release candidate]`` for the release candidate and
-     remove this for the actual release.
-     Note that, the Iris version and release date are updated automatically
-     when the documentation is built
-   * Review the file for correctness
-   * Work with the development team to populate the ``Release Highlights``
-     dropdown at the top of the file, which provides extra detail on notable
-     changes
-   * Use ``git`` to add and commit all changes, including removal of
-     ``latest.rst.template``.
-
-#. Update the ``whatsnew`` index ``docs/src/whatsnew/index.rst``
-
-   * Remove the reference to ``latest.rst``
-   * Add a reference to ``v1.9.rst`` to the top of the list
-
-#. Check your changes by building the documentation and reviewing
-#. Once all the above steps are complete, the release is cut, using
-   the :guilabel:`Draft a new release` button on the
-   `Iris release page <https://github.com/SciTools/iris/releases>`_
-   and targeting the release branch if it exists
-#. Create the release feature branch ``v1.9.x`` on `SciTools/iris`_ if it doesn't
-   already exist. For point/bugfix releases use the branch which already exists
-
-
-Post Release Steps
-~~~~~~~~~~~~~~~~~~
-
-#. Check the documentation has built on `Read The Docs`_.  The build is
-   triggered by any commit to ``main``.  Additionally check that the versions
-   available in the pop out menu in the bottom right corner include the new
-   release version.  If it is not present you will need to configure the
-   versions available in the **admin** dashboard in `Read The Docs`_.
-#. Review the `Active Versions`_ for the ``scitools-iris`` project on
-   `Read The Docs`_ to ensure that the appropriate versions are ``Active``
-   and/or ``Hidden``. To do this ``Edit`` the appropriate version e.g.,
-   see `Editing v3.0.0rc0`_ (must be logged into Read the Docs).
-#. Merge back to ``main``. This should be done after all releases, including
-   the release candidate, and also after major changes to the release branch.
-#. On main, make a new ``latest.rst`` from ``latest.rst.template`` and update
-   the include statement and the toctree in ``index.rst`` to point at the new
-   ``latest.rst``.
-#. Consider updating ``docs/src/userguide/citation.rst`` on ``main`` to include 
-   the version number, date and `Zenodo DOI <https://doi.org/10.5281/zenodo.595182>`_ 
-   of the new release. Ideally this would be updated before the release, but 
-   the DOI for the new version is only available once the release has been 
-   created in GitHub.
-
-
 .. _SciTools/iris: https://github.com/SciTools/iris
 .. _tag on the SciTools/Iris: https://github.com/SciTools/iris/releases
 .. _conda-forge Anaconda channel: https://anaconda.org/conda-forge/iris
@@ -294,5 +280,10 @@ Post Release Steps
 .. _rc_iris: https://anaconda.org/conda-forge/iris/labels
 .. _Generating Distribution Archives: https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives
 .. _Packaging Your Project: https://packaging.python.org/guides/distributing-packages-using-setuptools/#packaging-your-project
-.. _latest CF standard names: http://cfconventions.org/standard-names.html
+.. _latest CF standard names: http://cfconventions.org/Data/cf-standard-names/current/src/cf-standard-name-table.xml
 .. _setuptools-scm: https://github.com/pypa/setuptools_scm
+.. _Semantic Versioning: https://semver.org/
+.. _PEP 440: https://peps.python.org/pep-0440/
+.. _@scitools_iris: https://twitter.com/scitools_iris
+.. _GitHub Projects: https://github.com/SciTools/iris/projects
+.. _Zenodo DOI: https://doi.org/10.5281/zenodo.595182
diff --git a/docs/src/developers_guide/release_do_nothing.rst b/docs/src/developers_guide/release_do_nothing.rst
new file mode 100644
index 0000000000..1f72827184
--- /dev/null
+++ b/docs/src/developers_guide/release_do_nothing.rst
@@ -0,0 +1,12 @@
+:orphan:
+
+Release Do-Nothing Script
+-------------------------
+
+Rendered from the original ``<Iris repo root>/tools/release_do_nothing.py``.
+
+`Read more about do-nothing scripts
+<https://blog.danslimmon.com/2019/07/15/do-nothing-scripting-the-key-to-gradual-automation/>`_
+
+.. literalinclude:: ../../../tools/release_do_nothing.py
+   :language: python
diff --git a/tools/release_do_nothing.py b/tools/release_do_nothing.py
new file mode 100644
index 0000000000..c039a5b16d
--- /dev/null
+++ b/tools/release_do_nothing.py
@@ -0,0 +1,801 @@
+# Copyright Iris contributors
+#
+# This file is part of Iris and is released under the LGPL license.
+# See COPYING and COPYING.LESSER in the root of the repository for full
+# licensing details.
+"""
+A do-nothing script to hand-hold through the Iris release process.
+
+https://blog.danslimmon.com/2019/07/15/do-nothing-scripting-the-key-to-gradual-automation/
+
+"""
+from datetime import datetime
+from enum import Enum
+from pathlib import Path
+import re
+from sys import stderr
+from time import sleep
+import typing
+
+
+class ReleaseTypes(Enum):
+    MAJOR = 0
+    MINOR = 1
+    PATCH = 2
+
+
+valid_release_types = typing.Literal["major", "minor", "patch"]
+
+
+class ReleaseStrings:
+    """An easy way to pass the various flavours of release string between functions."""
+
+    def __init__(self, input_tag: str):
+        version_mask = r"v\d+\.\d+\.\d+\D*.*"
+        regex_101 = "https://regex101.com/r/dLVaNH/1"
+        if re.fullmatch(version_mask, input_tag) is None:
+            message = (
+                "Release tag does not match the input mask:\n"
+                f"{version_mask}\n"
+                f"({regex_101})"
+            )
+            raise ValueError(message)
+        else:
+            self.tag = input_tag  # v1.2.3rc0
+
+        self.series = ".".join(self.tag.split(".")[:2])  # v1.2
+        self.branch = self.series + ".x"  # v1.2.x
+        self.release = self.tag[1:]  # 1.2.3rc0
+
+
+class WhatsNewRsts:
+    """An easy way to pass the paths of various What's New files between functions."""
+
+    def __init__(self, release_strings: ReleaseStrings):
+        src_dir = Path(__file__).parents[1] / "docs" / "src"
+        whatsnew_dir = src_dir / "whatsnew"
+        assert whatsnew_dir.is_dir()
+
+        self.latest = whatsnew_dir / "latest.rst"
+        self.release = whatsnew_dir / (release_strings.series[1:] + ".rst")
+        self.index = whatsnew_dir / "index.rst"
+        self.template = self.latest.with_suffix(".rst.template")
+
+
+def _break_print(message: str):
+    print()
+    print(message)
+    # Help with flow/visibility by waiting 1secs before proceeding.
+    sleep(1)
+
+
+def _mark_section(section_number: int):
+    _break_print(f"SECTION {section_number} ...")
+
+
+def _get_input(message: str, expected_inputs: str) -> str:
+    _break_print(message)
+    return input(expected_inputs + " : ")
+
+
+def _wait_for_done(message: str):
+    _break_print(message)
+    done = False
+    while not done:
+        done = (
+            input("Step complete? y / [n] : ").casefold() == "y".casefold()
+        )
+
+
+def _report_problem(message: str):
+    print(message, file=stderr)
+    # To ensure correct sequencing of messages.
+    sleep(0.5)
+
+
+def get_release_type() -> ReleaseTypes:
+    release_type = None
+    release_types_str = " ".join(
+        [f"{m.name}={m.value}" for m in ReleaseTypes.__members__.values()]
+    )
+    message = "What type of release are you preparing?\nhttps://semver.org/"
+    while release_type is None:
+        input_type = _get_input(message, release_types_str)
+        try:
+            release_type = ReleaseTypes(int(input_type))
+        except ValueError:
+            _report_problem("Invalid release type. Please try again ...")
+    _break_print(f"{release_type} confirmed.")
+    return release_type
+
+
+def get_release_tag() -> ReleaseStrings:
+    # TODO: automate using setuptools_scm.
+    release_strings = None
+    message = (
+        "Input the release tag you are creating today, including any release "
+        "candidate suffix.\n"
+        "https://semver.org/\n"
+        "https://scitools-iris.readthedocs.io/en/latest/developers_guide/release.html?highlight=candidate#release-candidate"
+    )
+    while release_strings is None:
+        input_tag = _get_input(message, "e.g. v1.2.3rc0")
+        try:
+            release_strings = ReleaseStrings(input_tag)
+        except ValueError as err:
+            _report_problem(str(err))
+    return release_strings
+
+
+def check_release_candidate(
+    release_type: ReleaseTypes, release_strings: ReleaseStrings
+) -> bool:
+    is_release_candidate = "rc" in release_strings.tag
+
+    message = "Checking tag for release candidate: "
+    if is_release_candidate:
+        message += "DETECTED\nThis IS a release candidate."
+    else:
+        message += "NOT DETECTED\nThis IS NOT a release candidate."
+    _break_print(message)
+
+    if release_type == ReleaseTypes.PATCH and is_release_candidate:
+        message = (
+            "Release candidates are not expected for PATCH releases. "
+            "Are you sure you want to continue?"
+        )
+        if _get_input(message, "y / [n]").casefold() != "y".casefold():
+            exit()
+    return is_release_candidate
+
+
+def check_first_in_series(
+    release_type: ReleaseTypes,
+    release_strings: ReleaseStrings,
+    is_release_candidate: bool,
+) -> bool:
+    first_in_series = False
+    if release_type != ReleaseTypes.PATCH:
+        message = (
+            "Have there been any prior releases in the "
+            f"{release_strings.series} series, including release candidates?"
+        )
+        first_in_series = (
+            _get_input(message, "[y] / n").casefold() == "n".casefold()
+        )
+        if first_in_series:
+            _break_print("First in series confirmed.")
+            if not is_release_candidate:
+                message = (
+                    "The first release in a series is expected to be a "
+                    "release candidate, but this is not. Are you sure you "
+                    "want to continue?"
+                )
+                if _get_input(message, "y / [n]").casefold() != "y".casefold():
+                    exit()
+        else:
+            _break_print("Existing series confirmed.")
+    return first_in_series
+
+
+def update_standard_names(first_in_series: bool) -> None:
+    if first_in_series:
+        message = (
+            "Update the file ``etc/cf-standard-name-table.xml`` to the latest CF "
+            "standard names, via a new Pull Request.\n"
+            "(This is used during build to automatically generate the sourcefile "
+            "``lib/iris/std_names.py``).\n"
+            "Latest standard names:\n"
+            'wget "http://cfconventions.org/Data/cf-standard-names/current/src/cf-standard-name-table.xml";'
+        )
+        _wait_for_done(message)
+
+
+def check_deprecations(release_type: ReleaseTypes) -> None:
+    if release_type == ReleaseTypes.MAJOR:
+        message = (
+            "This is a MAJOR release - be sure to finalise all deprecations "
+            "and FUTUREs from previous releases, via a new Pull Request.\n"
+            "https://scitools-iris.readthedocs.io/en/latest/developers_guide/contributing_deprecations.html"
+        )
+        _wait_for_done(message)
+
+
+def _delete_local_branch(branch_name: str):
+    message = (
+        "Before the next step, avoid a name clash by deleting any "
+        "existing local branch, if one exists.\n"
+        f"git branch -D {branch_name};\n"
+        f"git push -d origin {branch_name};"
+    )
+    _wait_for_done(message)
+
+
+def create_release_branch(
+    release_strings: ReleaseStrings, first_in_series: bool
+) -> None:
+    # TODO: automate
+
+    _break_print("Release branch management ...")
+
+    if first_in_series:
+        message = (
+            "Visit https://github.com/SciTools/iris and create the"
+            f"``{release_strings.branch}`` release branch from ``main``."
+        )
+        _wait_for_done(message)
+
+    else:
+        message = (
+            "Cherry-pick any specific commits that are needed from ``main`` "
+            f"onto {release_strings.branch} , to get the CI passing.\n"
+            "E.g. a new dependency pin may have been introduced since "
+            f"{release_strings.branch} was last updated from ``main``.\n"
+            "DO NOT squash-merge - want to preserve the original commit SHA's."
+        )
+        _wait_for_done(message)
+
+
+def finalise_whats_new(
+    release_type: ReleaseTypes,
+    release_strings: ReleaseStrings,
+    is_release_candidate: bool,
+    first_in_series: bool,
+) -> WhatsNewRsts:
+    _break_print("What's New finalisation ...")
+
+    working_branch = release_strings.branch + ".updates"
+    _delete_local_branch(working_branch)
+    message = (
+        f"Checkout a local branch from the official {release_strings.branch} branch.\n"
+        "git fetch upstream;\n"
+        f"git checkout upstream/{release_strings.branch} -b "
+        f"{working_branch};"
+    )
+    _wait_for_done(message)
+
+    rsts = WhatsNewRsts(release_strings)
+
+    # TODO: automate
+    if first_in_series:
+        message = (
+            "'Cut' the What's New for the release.\n"
+            f"git mv {rsts.latest.absolute()} {rsts.release.absolute()};"
+        )
+        _wait_for_done(message)
+
+        message = (
+            f"In {rsts.index.absolute()}:\n"
+            f"Replace references to {rsts.latest.name} with {rsts.release.name}"
+        )
+        _wait_for_done(message)
+
+    _break_print(f"What's New file path = {rsts.release}")
+
+    if not release_type == ReleaseTypes.PATCH:
+        whatsnew_title = f"{release_strings.series} ({datetime.today().strftime('%d %b %Y')})"
+        if is_release_candidate:
+            whatsnew_title += " [release candidate]"
+        # TODO: automate
+        message = f"In {rsts.release.name}: set the page title to:\n{whatsnew_title}\n"
+        _wait_for_done(message)
+
+        message = (
+            f"Review {rsts.release.name} to ensure it is a good reflection of "
+            f"what is new in {release_strings.series}."
+        )
+        _wait_for_done(message)
+
+        message = (
+            "Work with the development team to populate the Release "
+            f"Highlights dropdown section at the top of {rsts.release.name}."
+        )
+        _wait_for_done(message)
+
+    else:
+        message = (
+            "Create a patch dropdown section at the top of "
+            f"{rsts.release.name}.\n"
+            f"See {rsts.template} for how this should be written."
+        )
+        _wait_for_done(message)
+
+    if first_in_series:
+        # TODO: automate
+        message = (
+            "Remove the What's New template file.\n"
+            f"git rm {rsts.template.absolute()};"
+        )
+        _wait_for_done(message)
+
+    message = (
+        "Commit and push all the What's New changes.\n"
+        f'git commit -am "Whats new updates for {release_strings.tag} .";\n'
+        f"git push -u origin {working_branch};"
+    )
+    _wait_for_done(message)
+
+    message = (
+        f"Follow the Pull Request process to get {working_branch} "
+        f"merged into upstream/{release_strings.branch} .\n"
+        "Make sure the documentation is previewed during this process."
+    )
+    _wait_for_done(message)
+
+    return rsts
+
+
+def cut_release(
+    release_strings: ReleaseStrings, is_release_candidate: bool
+) -> None:
+    _break_print("The release ...")
+
+    message = (
+        "Visit https://github.com/SciTools/iris/releases/new to open "
+        "a blank new-release web page."
+    )
+    _wait_for_done(message)
+
+    message = (
+        f"Select {release_strings.branch} as the Target.\n"
+        f"Input {release_strings.tag} as the new tag to create, and also as "
+        "the Release title."
+    )
+    _wait_for_done(message)
+
+    message = (
+        "Craft an appropriate release description in the main text box.\n"
+        "Be sure to mention the What's New entry, conda-forge and PyPI - you "
+        "will need to return later to make these into links.\n"
+        "Be careful to change the appropriate words if copying from a "
+        "previous release description."
+    )
+    _wait_for_done(message)
+
+    if is_release_candidate:
+        message = (
+            "This is a release candidate - include the following instructions "
+            "for Conda-installing:\n"
+            "conda install -c conda-forge/label/rc_iris iris;"
+        )
+        _wait_for_done(message)
+
+        message = (
+            "This is a release candidate - tick the box to set this as a "
+            "pre-release."
+        )
+        _wait_for_done(message)
+
+    else:
+        message = "Tick the box to set this as the latest release."
+        _wait_for_done(message)
+
+    message = "Click: Publish release !"
+    _wait_for_done(message)
+
+    message = (
+        "The CI will now run against this new tag, including automatically "
+        "publishing to PyPI."
+    )
+    _break_print(message)
+
+
+def check_rtd(
+    release_strings: ReleaseStrings, is_release_candidate: bool
+) -> None:
+    _break_print("Read the Docs checks ...")
+
+    message = (
+        "Visit https://readthedocs.org/projects/scitools-iris/versions/ and "
+        "make sure you are logged in."
+    )
+    _wait_for_done(message)
+
+    message = f"Set {release_strings.tag} to Active, un-Hidden."
+    _wait_for_done(message)
+
+    message = f"Set {release_strings.branch} to Active, Hidden."
+    _wait_for_done(message)
+
+    message = (
+        "Keep the latest 2 branch doc builds active - those formatted 0.0.x - "
+        "deactivate older ones."
+    )
+    _wait_for_done(message)
+
+    message = (
+        f"Visit https://scitools-iris.readthedocs.io/en/{release_strings.tag} "
+        "to confirm:\n\n"
+        "- The docs have rendered.\n"
+        f"- The version badge in the top left reads: {release_strings.tag} .\n"
+        "   (this demonstrates that setuptools_scm has worked correctly).\n"
+        "- The What's New looks correct.\n"
+        f"- {release_strings.tag} is available in RTD's version switcher.\n\n"
+        "NOTE: the docs can take several minutes to finish building."
+    )
+    if not is_release_candidate:
+        message += (
+            "- Selecting 'stable' in the version switcher also brings up the "
+            f"{release_strings.tag} render."
+        )
+    _wait_for_done(message)
+
+    message = (
+        f"Visit https://scitools-iris.readthedocs.io/en/{release_strings.branch} "
+        "to confirm:\n\n"
+        "- The docs have rendered\n"
+        f"- The version badge in the top left includes: {release_strings.branch} .\n"
+        f"- {release_strings.branch} is NOT available in RTD's version switcher.\n\n"
+        "NOTE: the docs can take several minutes to finish building."
+    )
+    _wait_for_done(message)
+
+
+def check_pypi(
+    release_strings: ReleaseStrings, is_release_candidate: bool
+) -> str:
+    _break_print("PyPI checks ...")
+    _break_print("If anything goes wrong, manual steps are in the documentation.")
+
+    message = (
+        "Confirm that the following URL is correctly populated:\n"
+        f"https://pypi.org/project/scitools-iris/{release_strings.release}/"
+    )
+    _wait_for_done(message)
+
+    message = (
+        f"Confirm that {release_strings.release} is at the top of this page:\n"
+        "https://pypi.org/project/scitools-iris/#history"
+    )
+    _wait_for_done(message)
+
+    if is_release_candidate:
+        message = (
+            f"Confirm that {release_strings.release} is marked as a "
+            f"pre-release on this page:\n"
+            "https://pypi.org/project/scitools-iris/#history"
+        )
+    else:
+        message = (
+            f"Confirm that {release_strings.release} is the tag shown on the "
+            "scitools-iris PyPI homepage:\n"
+            "https://pypi.org/project/scitools-iris/"
+        )
+    _wait_for_done(message)
+
+    message = (
+        f"Visit the below and click `view hashes` for the Source Distribution"
+        f"(`.tar.gz`):\n"
+        f"https://pypi.org/project/scitools-iris/{release_strings.release}#files\n"
+    )
+    sha256 = _get_input(message, "Input the SHA256 hash")
+
+    message = (
+        "Confirm that pip install works as expected:\n"
+        f"pip install scitools-iris=={release_strings.release};"
+    )
+    _wait_for_done(message)
+
+    return sha256
+
+
+def update_conda_forge(
+    release_strings: ReleaseStrings, is_release_candidate: bool, sha256: str
+) -> None:
+    _break_print("conda-forge updates ...")
+
+    if not is_release_candidate:
+        message = (
+            "NOTE: after several hours conda-forge automation will create a "
+            "Pull Request against conda-forge/iris-feedstock (via the "
+            "regro-cf-autotick-bot). Quicker to sort it now, manually ..."
+        )
+        _break_print(message)
+
+    message = (
+        "Make sure you have a GitHub fork of:\n"
+        "https://github.com/conda-forge/iris-feedstock"
+    )
+    _wait_for_done(message)
+
+    message = (
+        "Make sure you have a local clone of your iris-feedstock fork.\n"
+        "`cd` into your clone."
+    )
+    _wait_for_done(message)
+
+    if is_release_candidate:
+        upstream_branch = "release-candidate"
+    else:
+        upstream_branch = "main"
+
+    # TODO: automate
+    message = (
+        "Checkout a new branch for the conda-forge changes for this release:\n"
+        "git fetch upstream;\n"
+        f"git checkout upstream/{upstream_branch} -b {release_strings.tag};\n"
+    )
+    _wait_for_done(message)
+
+    message = (
+        "Update ./recipe/meta.yaml:\n\n"
+        f"- The version at the very top of the file: {release_strings.release}\n"
+        f"- The sha256 hash: {sha256}\n"
+        "- Requirements: align the packages and pins with those in the Iris repo\n"
+        "- Maintainers: update with any changes to the dev team\n"
+        "- MAKE SURE everything else is correct - plenty of other things "
+        "might need one-off changes.\n"
+    )
+    _wait_for_done(message)
+
+    # TODO: automate
+    message = (
+        "No other file normally needs changing in iris-feedstock, so push up "
+        "the changes to prepare for a Pull Request:\n"
+        f'git commit -am "Recipe updates for {release_strings.tag} .";\n'
+        f"git push -u origin {release_strings.tag};"
+    )
+    _wait_for_done(message)
+
+    message = (
+        f"Follow the Pull Request process to get {release_strings.tag} branch "
+        f"merged into upstream/{upstream_branch} .\n"
+        "Specific conda-forge guidance will be automatically given once the "
+        "PR is created."
+    )
+    _wait_for_done(message)
+
+    message = (
+        f"Confirm that {release_strings.release} appears in this list:\n"
+        "https://anaconda.org/conda-forge/iris/files"
+    )
+    _wait_for_done(message)
+
+    if not is_release_candidate:
+        message = (
+            f"Confirm that {release_strings.release} is displayed on this "
+            "page as the latest available:\n"
+            "https://anaconda.org/conda-forge/iris"
+        )
+        _wait_for_done(message)
+
+    if is_release_candidate:
+        channel_command = " -c conda-forge/label/rc_iris "
+    else:
+        channel_command = " "
+    message = (
+        "Confirm that conda (or mamba) install works as expected:\n"
+        f"conda create -n tmp_iris{channel_command}iris={release_strings.release};\n"
+        f"conda remove -n tmp_iris --all;"
+    )
+    _wait_for_done(message)
+
+
+def update_links(release_strings: ReleaseStrings) -> None:
+    _break_print("Link updates ...")
+
+    message = (
+        "Revisit the GitHub release:\n"
+        f"https://github.com/SciTools/iris/releases/tag/{release_strings.tag}\n"
+        "You have confirmed that Read the Docs, PyPI and conda-forge have all "
+        "updated correctly. Include the following links in the release "
+        "notes:\n\n"
+        f"https://scitools-iris.readthedocs.io/en/{release_strings.tag}/\n"
+        f"https://pypi.org/project/scitools-iris/{release_strings.release}/\n"
+        f"https://anaconda.org/conda-forge/iris?version={release_strings.release}\n"
+    )
+    _wait_for_done(message)
+
+    message = (
+        "Update the release page in GitHub discussions, with the above links "
+        "and anything else appropriate.\n"
+        "https://github.com/SciTools/iris/discussions"
+    )
+    _wait_for_done(message)
+
+
+def twitter_announce(
+    release_strings: ReleaseStrings, first_in_series: bool
+) -> None:
+    message = (
+        "Announce the release via https://twitter.com/scitools_iris, and any "
+        "other appropriate message boards (e.g. Yammer).\n"
+        "Any content used for the announcement should be stored in the "
+        "SciTools/twitter-scitools-iris GitHub repo.\n"
+    )
+    if not first_in_series:
+        message += (
+            f"Consider replying within an existing {release_strings.series} "
+            "announcement thread, if appropriate."
+        )
+    _wait_for_done(message)
+
+
+def update_citation(
+    release_strings: ReleaseStrings, is_release_candidate: bool
+) -> None:
+    if not is_release_candidate:
+        src_dir = Path(__file__).parents[1] / "docs" / "src"
+        citation_rst = src_dir / "userguide" / "citation.rst"
+        assert citation_rst.is_file()
+        message = (
+            f"Follow the Pull Request process to update {citation_rst.name} "
+            "with the correct dates, DOI and version string for "
+            f"{release_strings.tag}.\n"
+            f"{citation_rst.absolute()}\n\n"
+            f"The PR should target {release_strings.branch} (prior to merge-back)."
+        )
+        _wait_for_done(message)
+
+
+def merge_back(
+    release_strings: ReleaseStrings, first_in_series: bool, rsts: WhatsNewRsts
+) -> None:
+    _break_print("Branch merge-back ...")
+
+    merge_commit = (
+        "BE SURE TO MERGE VIA A MERGE-COMMIT (not a squash-commit), to "
+        "preserve the commit SHA's."
+    )
+
+    if first_in_series:
+        # TODO: automate
+
+        working_branch = release_strings.branch + ".mergeback"
+        _delete_local_branch(working_branch)
+        message = (
+            "Checkout a local branch from the official ``main`` branch.\n"
+            "git fetch upstream;\n"
+            f"git checkout upstream/main -b {working_branch};"
+        )
+        _wait_for_done(message)
+
+        message = (
+            f"Merge in the commits from {release_strings.branch}.\n"
+            f"{merge_commit}\n"
+            f"git merge upstream/{release_strings.branch} --no-ff"
+            '-m "Merging release branch into main";'
+        )
+        _wait_for_done(message)
+
+        message = (
+            "Recreate the following files, which are present in ``main``, but "
+            f"are currently deleted from {working_branch}:\n"
+            f"{rsts.latest.absolute()}\n"
+            f"{rsts.template.absolute()}\n"
+            "THEN:\n"
+            f"git add {rsts.latest.absolute()};\n"
+            f"git add {rsts.template.absolute()};\n"
+        )
+        _wait_for_done(message)
+
+        message = (
+            f"In {rsts.index.absolute()}:\n"
+            f"Add {rsts.latest.name} to the top of the list of .rst files, "
+            f"and set the top include:: to be {rsts.latest.name} ."
+        )
+        _wait_for_done(message)
+
+        message = (
+            "Commit and push all the What's New changes.\n"
+            'git commit -am "Restore latest Whats New files.";\n'
+            f"git push -u origin {working_branch};"
+        )
+        _wait_for_done(message)
+
+        message = (
+            "Follow the Pull Request process to get "
+            f"{working_branch} merged into upstream/main .\n"
+            "Make sure the documentation is previewed during this process.\n"
+            f"{merge_commit}"
+        )
+        _wait_for_done(message)
+
+    else:
+        message = (
+            f"Propose a merge-back from {release_strings.branch} into ``main`` by "
+            f"visiting this URL and clicking `Create pull request`:\n"
+            f"https://github.com/SciTools/iris/compare/main...{release_strings.branch}\n"
+            f"{merge_commit}"
+        )
+        _wait_for_done(message)
+
+
+def main():
+    _mark_section(1)
+    release_type = get_release_type()
+
+    _mark_section(2)
+    release_strings = get_release_tag()
+
+    _mark_section(3)
+    is_release_candidate = check_release_candidate(
+        release_type,
+        release_strings,
+    )
+
+    _mark_section(4)
+    is_first_in_series = check_first_in_series(
+        release_type,
+        release_strings,
+        is_release_candidate,
+    )
+
+    _mark_section(5)
+    update_standard_names(
+        is_first_in_series,
+    )
+
+    _mark_section(6)
+    check_deprecations(
+        release_type,
+    )
+
+    _mark_section(7)
+    create_release_branch(
+        release_strings,
+        is_first_in_series,
+    )
+
+    _mark_section(8)
+    whats_new_rsts = finalise_whats_new(
+        release_type,
+        release_strings,
+        is_release_candidate,
+        is_first_in_series,
+    )
+
+    _mark_section(9)
+    cut_release(
+        release_strings,
+        is_release_candidate,
+    )
+
+    _mark_section(10)
+    check_rtd(
+        release_strings,
+        is_release_candidate,
+    )
+
+    _mark_section(11)
+    sha256 = check_pypi(
+        release_strings,
+        is_release_candidate,
+    )
+
+    _mark_section(12)
+    update_conda_forge(
+        release_strings,
+        is_release_candidate,
+        sha256,
+    )
+
+    _mark_section(13)
+    update_links(
+        release_strings,
+    )
+
+    _mark_section(14)
+    twitter_announce(
+        release_strings,
+        is_first_in_series,
+    )
+
+    _mark_section(15)
+    update_citation(
+        release_strings,
+        is_release_candidate,
+    )
+
+    _mark_section(16)
+    merge_back(
+        release_strings,
+        is_first_in_series,
+        whats_new_rsts,
+    )
+
+    _break_print("RELEASE COMPLETE. Congratulations! 🎉")
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index 36b863b661..b9ef598048 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -74,6 +74,11 @@ This document explains the changes made to Iris for this release
    during development. (:pull:`5258`)
 
 
+#. `@trexfeathers`_ re-wrote the :ref:`iris_development_releases` documentation
+   for clarity, and wrote a step-by-step
+   :doc:`/developers_guide/release_do_nothing` for the release process.
+   (:pull:`5134`)
+
 💼 Internal
 ===========