Set up 2.0 pre-releases
diff --git a/.azure-pipelines/2.0-prerelease.yml b/.azure-pipelines/2.0-prerelease.yml
new file mode 100644
index 00000000000..2cdcf8f3035
--- /dev/null
+++ b/.azure-pipelines/2.0-prerelease.yml
@@ -0,0 +1,18 @@
+# Pipeline for testing, building, and deploying Certbot 2.0 pre-releases.
+trigger: none
+pr: none
+
+variables:
+  # We don't publish our Docker images in this pipeline, but when building them
+  # for testing, let's use the nightly tag.
+  dockerTag: nightly
+  snapBuildTimeout: 5400
+
+stages:
+  - template: templates/stages/test-and-package-stage.yml
+  - stage: DeploySnaps
+    jobs:
+      - template: templates/jobs/snap-deploy-job.yml
+        parameters:
+          snapReleaseChannel: beta
+  - template: templates/stages/notify-failure-stage.yml
diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml
index 26639151f95..9169dc950f9 100644
--- a/.azure-pipelines/release.yml
+++ b/.azure-pipelines/release.yml
@@ -15,5 +15,5 @@ stages:
   - template: templates/stages/changelog-stage.yml
   - template: templates/stages/deploy-stage.yml
     parameters:
-      snapReleaseChannel: beta
+      snapReleaseChannel: candidate
   - template: templates/stages/notify-failure-stage.yml
diff --git a/.azure-pipelines/templates/jobs/snap-deploy-job.yml b/.azure-pipelines/templates/jobs/snap-deploy-job.yml
new file mode 100644
index 00000000000..d1d709cb99d
--- /dev/null
+++ b/.azure-pipelines/templates/jobs/snap-deploy-job.yml
@@ -0,0 +1,75 @@
+# As (somewhat) described at
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#context,
+# each template only has access to the parameters passed into it. To help make
+# use of this design, we define snapReleaseChannel without a default value
+# which requires the user of this template to define it as described at
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/parameters-name?view=azure-pipelines#remarks.
+# This makes the user of this template be explicit while allowing them to
+# define their own parameters with defaults that make sense for that context.
+parameters:
+- name: snapReleaseChannel
+  type: string
+  values:
+  - edge
+  - beta
+  - candidate
+
+jobs:
+  # This job relies on credentials used to publish the Certbot snaps. This
+  # credential file was created by running:
+  #
+  #   snapcraft logout
+  #   snapcraft export-login --channels=candidate,beta,edge snapcraft.cfg
+  #     (provide the shared snapcraft credentials when prompted)
+  #
+  # Then the file was added as a secure file in Azure pipelines
+  # with the name snapcraft.cfg by following the instructions at
+  # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops
+  # including authorizing the file for use in the "nightly" and "release"
+  # pipelines as described at
+  # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops#q-how-do-i-authorize-a-secure-file-for-use-in-a-specific-pipeline.
+  #
+  # This file has a maximum lifetime of one year and the current file will
+  # expire on 2023-09-06. The file will need to be updated before then to
+  # prevent automated deploys from breaking.
+  #
+  # Revoking these credentials can be done by changing the password of the
+  # account used to generate the credentials. See
+  # https://forum.snapcraft.io/t/revoking-exported-credentials/19031 for
+  # more info.
+  - job: publish_snap
+    pool:
+      vmImage: ubuntu-22.04
+    variables:
+      - group: certbot-common
+    strategy:
+      matrix:
+        amd64:
+          SNAP_ARCH: amd64
+        arm32v6:
+          SNAP_ARCH: armhf
+        arm64v8:
+          SNAP_ARCH: arm64
+    steps:
+      - bash: |
+          set -e
+          sudo apt-get update
+          sudo apt-get install -y --no-install-recommends snapd
+          sudo snap install --classic snapcraft
+        displayName: Install dependencies
+      - task: DownloadPipelineArtifact@2
+        inputs:
+          artifact: snaps_$(SNAP_ARCH)
+          path: $(Build.SourcesDirectory)/snap
+        displayName: Retrieve Certbot snaps
+      - task: DownloadSecureFile@1
+        name: snapcraftCfg
+        inputs:
+          secureFile: snapcraft.cfg
+      - bash: |
+          set -e
+          export SNAPCRAFT_STORE_CREDENTIALS=$(cat "$(snapcraftCfg.secureFilePath)")
+          for SNAP_FILE in snap/*.snap; do
+            tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} "${SNAP_FILE}"
+          done
+        displayName: Publish to Snap store
diff --git a/.azure-pipelines/templates/stages/deploy-stage.yml b/.azure-pipelines/templates/stages/deploy-stage.yml
index 8144f4beeb1..cb8b24e2846 100644
--- a/.azure-pipelines/templates/stages/deploy-stage.yml
+++ b/.azure-pipelines/templates/stages/deploy-stage.yml
@@ -1,74 +1,16 @@
 parameters:
+# We do not define acceptable values for this parameter here as it is passed
+# through to ../jobs/snap-deploy-job.yml which does its own sanity checking.
 - name: snapReleaseChannel
   type: string
   default: edge
-  values:
-  - edge
-  - beta
 
 stages:
   - stage: Deploy
     jobs:
-      # This job relies on credentials used to publish the Certbot snaps. This
-      # credential file was created by running:
-      #
-      #   snapcraft logout
-      #   snapcraft export-login --channels=beta,edge snapcraft.cfg
-      #     (provide the shared snapcraft credentials when prompted)
-      #
-      # Then the file was added as a secure file in Azure pipelines
-      # with the name snapcraft.cfg by following the instructions at
-      # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops
-      # including authorizing the file for use in the "nightly" and "release"
-      # pipelines as described at
-      # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops#q-how-do-i-authorize-a-secure-file-for-use-in-a-specific-pipeline.
-      #
-      # This file has a maximum lifetime of one year and the current
-      # file will expire on 2023-06-17 which is also tracked by
-      # https://github.com/certbot/certbot/issues/7931. The file will
-      # need to be updated before then to prevent automated deploys
-      # from breaking.
-      #
-      # Revoking these credentials can be done by changing the password of the
-      # account used to generate the credentials. See
-      # https://forum.snapcraft.io/t/revoking-exported-credentials/19031 for
-      # more info.
-      - job: publish_snap
-        pool:
-          vmImage: ubuntu-22.04
-        variables:
-          - group: certbot-common
-        strategy:
-          matrix:
-            amd64:
-              SNAP_ARCH: amd64
-            arm32v6:
-              SNAP_ARCH: armhf
-            arm64v8:
-              SNAP_ARCH: arm64
-        steps:
-          - bash: |
-              set -e
-              sudo apt-get update
-              sudo apt-get install -y --no-install-recommends snapd
-              sudo snap install --classic snapcraft
-            displayName: Install dependencies
-          - task: DownloadPipelineArtifact@2
-            inputs:
-              artifact: snaps_$(SNAP_ARCH)
-              path: $(Build.SourcesDirectory)/snap
-            displayName: Retrieve Certbot snaps
-          - task: DownloadSecureFile@1
-            name: snapcraftCfg
-            inputs:
-              secureFile: snapcraft.cfg
-          - bash: |
-              set -e
-              export SNAPCRAFT_STORE_CREDENTIALS=$(cat $(snapcraftCfg.secureFilePath))
-              for SNAP_FILE in snap/*.snap; do
-                tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} "${SNAP_FILE}"
-              done
-            displayName: Publish to Snap store
+      - template: ../jobs/snap-deploy-job.yml
+        parameters:
+          snapReleaseChannel: ${{ parameters.snapReleaseChannel }}
       - job: publish_docker
         pool:
           vmImage: ubuntu-22.04
diff --git a/tools/finish_release.py b/tools/finish_release.py
index ec749d48f6a..18aa8ee30ee 100755
--- a/tools/finish_release.py
+++ b/tools/finish_release.py
@@ -4,7 +4,7 @@
 
 This currently includes:
 
-* Moving snaps from the beta channel to the stable channel
+* Moving snaps from the candidate channel to the stable channel
 * Publishing the Windows installer in a GitHub release
 
 Setup:
@@ -110,7 +110,7 @@ def assert_logged_into_snapcraft():
 
 
 def get_snap_revisions(snap, version):
-    """Finds the revisions for the snap and version in the beta channel.
+    """Finds the revisions for the snap and version in the candidate channel.
 
     If you call this function without being logged in with snapcraft, it
     will hang with no output.
@@ -130,20 +130,20 @@ def get_snap_revisions(snap, version):
     print('Getting revision numbers for', snap, version)
     cmd = ['snapcraft', 'status', snap]
     process = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, universal_newlines=True)
-    pattern = f'^\s+beta\s+{version}\s+(\d+)\s*'
+    pattern = f'^\s+candidate\s+{version}\s+(\d+)\s*'
     revisions = re.findall(pattern, process.stdout, re.MULTILINE)
     assert len(revisions) == SNAP_ARCH_COUNT, f'Unexpected number of snaps found for {snap} {version} (expected {SNAP_ARCH_COUNT}, found {len(revisions)})'
     return revisions
 
 
 def promote_snaps(version):
-    """Promotes all Certbot snaps from the beta to stable channel.
+    """Promotes all Certbot snaps from the candidate to stable channel.
 
     If the snaps have already been released to the stable channel, this
     function will try to release them again which has no effect.
 
     :param str version: the version number that should be found in the
-        beta channel, e.g. 1.7.0
+        candidate channel, e.g. 1.7.0
 
     :raises SystemExit: if the command snapcraft is unavailable or it
         isn't logged into an account

This implements the majority of https://docs.google.com/document/d/1t02y0qQvTgc4eNPY4OwfYu7oW41DEBNhr8fq_O97kXo/edit?usp=sharing. The work that remains after this is merged into master is merging this into the 2.0 branch, setting up the 2.0-prerelease pipeline for the 2.0 branch, writing documentation on how to use that pipeline, doing a prerelease, and writing a post on the community forum.

One change I made in this PR that's not easily seen/understandable is I removed the reference to https://github.com/certbot/certbot/issues/7931 for the reason I wrote at https://github.com/certbot/certbot/issues/7931#issuecomment-1238650122.

I also set up a dummy 2.0 prerelease pipeline to test this at https://dev.azure.com/certbot/certbot/_build?definitionId=6&_a=summary which seems to be working properly.