2020-10-08 18:18:09 -04:00
#!/usr/bin/env python
"""
Post - release script to publish artifacts created from Azure Pipelines .
This currently includes :
2023-02-09 21:41:16 -05:00
* Moving snaps from the beta channel to the stable channel
2020-10-08 18:18:09 -04:00
* Publishing the Windows installer in a GitHub release
Setup :
- Install the snapcraft command line tool and log in to a privileged account .
- https : / / snapcraft . io / docs / installing - snapcraft
- Use the command ` snapcraft login ` to log in .
Run :
2022-06-29 18:52:50 -04:00
python tools / finish_release . py - - css < URL of code signing server >
2021-12-21 11:28:31 -05:00
Testing :
This script can be safely run between releases . When this is done , the script
should execute successfully until the final step when it tries to set draft
equal to false on the GitHub release . This step should fail because a published
release with that name already exists .
2020-10-08 18:18:09 -04:00
"""
2020-12-10 18:13:48 -05:00
import argparse
2023-02-10 13:51:20 -05:00
import getpass
2020-10-08 18:18:09 -04:00
import glob
import os . path
import re
import subprocess
import sys
import tempfile
from zipfile import ZipFile
2023-02-10 13:51:20 -05:00
from azure . devops . connection import Connection
2020-10-08 18:18:09 -04:00
import requests
# Path to the root directory of the Certbot repository containing this script
REPO_ROOT = os . path . dirname ( os . path . dirname ( os . path . realpath ( __file__ ) ) )
2023-02-09 21:41:16 -05:00
# This list contains the names of all Certbot DNS plugins. We used to have a
# CloudXNS plugin and since it's possible devs still have that directory
# locally, we filter it out here. If it's included in this list, this script
# will crash later when it fails to find a CloudXNS snap on the snap store with
# the current version since we no longer build it.
PLUGIN_SNAPS = [ os . path . basename ( path )
for path in glob . glob ( os . path . join ( REPO_ROOT , ' certbot-dns-* ' ) )
2023-02-09 22:35:37 -05:00
if not path . endswith ( ' certbot-dns-cloudxns ' ) ]
2020-10-08 18:18:09 -04:00
# This list contains the name of all Certbot snaps that should be published to
# the stable channel.
2023-01-11 15:27:38 -05:00
ALL_SNAPS = [ ' certbot ' ] + PLUGIN_SNAPS
2020-10-08 18:18:09 -04:00
# This is the count of the architectures currently supported by our snaps used
# for sanity checking.
SNAP_ARCH_COUNT = 3
2020-12-10 18:13:48 -05:00
def parse_args ( args ) :
""" Parse command line arguments.
: param args : command line arguments with the program name removed . This is
usually taken from sys . argv [ 1 : ] .
: type args : ` list ` of ` str `
: returns : parsed arguments
: rtype : argparse . Namespace
"""
# Use the file's docstring for the help text and don't let argparse reformat it.
parser = argparse . ArgumentParser ( description = __doc__ ,
formatter_class = argparse . RawDescriptionHelpFormatter )
2023-02-09 21:41:16 -05:00
parser . add_argument ( ' --css ' , type = str , required = True , help = ' hostname of code signing server ' )
2020-12-10 18:13:48 -05:00
return parser . parse_args ( args )
2023-10-16 20:54:24 -04:00
2022-06-29 18:52:50 -04:00
def publish_windows ( css ) :
""" SSH into CSS and trigger downloading Azure Pipeline assets, sign, and upload to Github
2020-12-10 18:13:48 -05:00
2022-06-29 18:52:50 -04:00
: param str css : CSS host name
2020-10-08 18:18:09 -04:00
"""
2022-07-08 15:27:50 -04:00
username = input ( " CSS username (usually EFF username): " )
2022-06-29 18:52:50 -04:00
host = css
command = " ssh -t {} @ {} bash /opt/certbot-misc/css/venv.sh " . format ( username , host )
2023-10-16 20:54:24 -04:00
2022-06-29 18:52:50 -04:00
print ( " SSH into CSS to trigger signing and uploading of Windows installer... " )
subprocess . run ( command , check = True , universal_newlines = True , shell = True )
2020-10-08 18:18:09 -04:00
def assert_logged_into_snapcraft ( ) :
""" Confirms that snapcraft is logged in to an account.
: raises SystemExit : if the command snapcraft is unavailable or it
isn ' t logged into an account
"""
cmd = ' snapcraft whoami ' . split ( )
try :
subprocess . run ( cmd , check = True , stdout = subprocess . DEVNULL ,
stderr = subprocess . DEVNULL , universal_newlines = True )
except ( subprocess . CalledProcessError , OSError ) :
print ( " Please make sure that the command line tool snapcraft is " )
print ( " installed and that you have logged in to an account by running " )
print ( " ' snapcraft login ' . " )
sys . exit ( 1 )
2023-01-11 15:27:38 -05:00
def get_snap_revisions ( snap , channel , version ) :
""" Finds the revisions for the snap and version in the given channel.
2020-10-08 18:18:09 -04:00
If you call this function without being logged in with snapcraft , it
will hang with no output .
: param str snap : the name of the snap on the snap store
2023-01-11 15:27:38 -05:00
: param str channel : snap channel to pull revisions from
2020-10-08 18:18:09 -04:00
: param str version : snap version number , e . g . 1.7 .0
: returns : list of revision numbers
: rtype : ` list ` of ` str `
: raises subprocess . CalledProcessError : if the snapcraft command
fails
: raises AssertionError : if the expected snaps are not found
"""
print ( ' Getting revision numbers for ' , snap , version )
cmd = [ ' snapcraft ' , ' status ' , snap ]
process = subprocess . run ( cmd , check = True , stdout = subprocess . PIPE , universal_newlines = True )
2023-01-11 15:27:38 -05:00
pattern = f ' ^ \ s+ { channel } \ s+ { version } \ s+( \ d+) \ s* '
2020-10-08 18:18:09 -04:00
revisions = re . findall ( pattern , process . stdout , re . MULTILINE )
2023-01-11 15:27:38 -05:00
assert len ( revisions ) == SNAP_ARCH_COUNT , f ' Unexpected number of snaps found for { channel } { snap } { version } (expected { SNAP_ARCH_COUNT } , found { len ( revisions ) } ) '
2020-10-08 18:18:09 -04:00
return revisions
2023-01-11 15:27:38 -05:00
def promote_snaps ( snaps , source_channel , version , progressive_percentage = None ) :
""" Promotes the given snaps from source_channel to the stable channel.
2020-10-08 18:18:09 -04:00
If the snaps have already been released to the stable channel , this
function will try to release them again which has no effect .
2023-01-11 15:27:38 -05:00
: param snaps : snap package names to be promoted
: type snaps : ` list ` of ` str `
: param str source_channel : snap channel to promote from
2020-10-08 18:18:09 -04:00
: param str version : the version number that should be found in the
2022-09-09 17:23:39 -04:00
candidate channel , e . g . 1.7 .0
2023-01-11 15:27:38 -05:00
: param progressive_percentage : specifies the percentage of a progressive
deployment
: type progressive_percentage : int or None
2020-10-08 18:18:09 -04:00
: raises SystemExit : if the command snapcraft is unavailable or it
isn ' t logged into an account
: raises subprocess . CalledProcessError : if a snapcraft command fails
for another reason
"""
assert_logged_into_snapcraft ( )
2023-01-11 15:27:38 -05:00
for snap in snaps :
revisions = get_snap_revisions ( snap , source_channel , version )
2020-10-08 19:38:05 -04:00
# The loop below is kind of slow, so let's print some output about what
# it is doing.
print ( ' Releasing ' , snap , ' snaps to the stable channel ' )
2020-10-08 18:18:09 -04:00
for revision in revisions :
cmd = [ ' snapcraft ' , ' release ' , snap , revision , ' stable ' ]
2023-01-11 15:27:38 -05:00
if progressive_percentage :
cmd . extend ( f ' --progressive { progressive_percentage } ' . split ( ) )
2020-10-08 18:18:09 -04:00
try :
subprocess . run ( cmd , check = True , stdout = subprocess . PIPE , universal_newlines = True )
except subprocess . CalledProcessError as e :
print ( " The command " , f " ' { ' ' . join ( cmd ) } ' " , " failed. " )
print ( " The output printed to stdout was: " )
print ( e . stdout )
raise
2023-02-09 21:41:16 -05:00
def fetch_version_number ( major_version = None ) :
2022-06-29 18:52:50 -04:00
""" Retrieve version number for release from Azure Pipelines
2023-01-11 15:27:38 -05:00
: param major_version : only consider releases for the specified major
version
: type major_version : str or None
2022-06-29 18:52:50 -04:00
: returns : version number
2023-10-16 20:54:24 -04:00
2022-06-29 18:52:50 -04:00
"""
# Create a connection to the azure org
organization_url = ' https://dev.azure.com/certbot '
connection = Connection ( base_url = organization_url )
2023-10-16 20:54:24 -04:00
2022-06-29 18:52:50 -04:00
# Find the build artifacts
build_client = connection . clients . get_build_client ( )
2023-10-16 20:54:24 -04:00
builds = build_client . get_builds ( ' certbot ' , definitions = ' 3 ' )
for build in builds :
2023-01-11 15:27:38 -05:00
version = build_client . get_build ( ' certbot ' , build . id ) . source_branch . split ( ' v ' ) [ 1 ]
if major_version is None or version . split ( ' . ' ) [ 0 ] == major_version :
return version
raise ValueError ( ' Release not found on Azure Pipelines! ' )
2020-10-08 18:18:09 -04:00
def main ( args ) :
2020-12-10 18:13:48 -05:00
parsed_args = parse_args ( args )
2020-10-08 18:18:09 -04:00
2022-06-29 18:52:50 -04:00
css = parsed_args . css
2023-02-09 21:41:16 -05:00
version = fetch_version_number ( )
2022-06-29 18:52:50 -04:00
# Once the GitHub release has been published, trying to publish it
# again fails. Publishing the snaps can be done multiple times though
# so we do that first to make it easier to run the script again later
2023-10-16 20:54:24 -04:00
# if something goes wrong.
2023-02-09 21:41:16 -05:00
promote_snaps ( ALL_SNAPS , ' beta ' , version )
publish_windows ( css )
2020-10-08 18:18:09 -04:00
if __name__ == " __main__ " :
main ( sys . argv [ 1 : ] )