1+ # This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid
2+ # having to use a regular user account.
3+ import subprocess
4+ import sys
5+ import time
6+
7+ # Install our own dependencies
8+ subprocess .check_call ([sys .executable , '-m' , 'pip' , 'install' , 'jwt' ])
9+
110import argparse
211import subprocess
312import sys
413import os
514import urllib .request
615import base64
716import re
17+ import json
18+ import jwt
819
920# If this script is not being run as part of an Octopus step, print directly to std out.
1021if "printverbose" not in globals ():
@@ -67,7 +78,8 @@ def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose
6778 return stdout , stderr , retcode
6879
6980
70- def check_repo_exists (url , username , password ):
81+ def check_repo_exists (git_protocol , git_host , git_organization , new_repo , username , password ):
82+ url = git_protocol + '://' + git_host + '/' + git_organization + '/' + new_repo + '.git'
7183 try :
7284 auth = base64 .b64encode ((username + ':' + password ).encode ('ascii' ))
7385 auth_header = "Basic " + auth .decode ('ascii' )
@@ -81,6 +93,52 @@ def check_repo_exists(url, username, password):
8193 return False
8294
8395
96+ def check_github_repo_exists (git_organization , new_repo , username , password ):
97+ url = 'https://api.github.com/repos/' + git_organization + '/' + new_repo
98+ try :
99+ auth = base64 .b64encode ((username + ':' + password ).encode ('ascii' ))
100+ auth_header = "Basic " + auth .decode ('ascii' )
101+ headers = {
102+ "Authorization" : auth_header
103+ }
104+ request = urllib .request .Request (url , headers = headers )
105+ urllib .request .urlopen (request )
106+ return True
107+ except :
108+ return False
109+
110+
111+ def generate_github_token (github_app_id , github_app_private_key , github_app_installation_id ):
112+ # Generate the tokens used by git and the GitHub API
113+ app_id = github_app_id
114+ signing_key = jwt .jwk_from_pem (github_app_private_key .encode ('utf-8' ))
115+
116+ payload = {
117+ # Issued at time
118+ 'iat' : int (time .time ()),
119+ # JWT expiration time (10 minutes maximum)
120+ 'exp' : int (time .time ()) + 600 ,
121+ # GitHub App's identifier
122+ 'iss' : app_id
123+ }
124+
125+ # Create JWT
126+ jwt_instance = jwt .JWT ()
127+ encoded_jwt = jwt_instance .encode (payload , signing_key , alg = 'RS256' )
128+
129+ # Create access token
130+ url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'
131+ headers = {
132+ 'Authorization' : 'Bearer ' + encoded_jwt ,
133+ 'Accept' : 'application/vnd.github+json' ,
134+ 'X-GitHub-Api-Version' : '2022-11-28'
135+ }
136+ request = urllib .request .Request (url , headers = headers , method = 'POST' )
137+ response = urllib .request .urlopen (request )
138+ response_json = json .loads (response .read ().decode ())
139+ return response_json ['token' ]
140+
141+
84142def init_argparse ():
85143 parser = argparse .ArgumentParser (
86144 usage = '%(prog)s [OPTION] [FILE]...' ,
@@ -114,6 +172,15 @@ def init_argparse():
114172 action = 'store' ,
115173 default = get_octopusvariable_quiet ('Git.Url.Organization' ) or get_octopusvariable_quiet (
116174 'MergeRepo.Git.Url.Organization' ))
175+ parser .add_argument ('--github-app-id' , action = 'store' ,
176+ default = get_octopusvariable_quiet ('GitHub.App.Id' ) or get_octopusvariable_quiet (
177+ 'PreviewMerge.GitHub.App.Id' ))
178+ parser .add_argument ('--github-app-installation-id' , action = 'store' ,
179+ default = get_octopusvariable_quiet ('GitHub.App.InstallationId' ) or get_octopusvariable_quiet (
180+ 'PreviewMerge.GitHub.App.InstallationId' ))
181+ parser .add_argument ('--github-app-private-key' , action = 'store' ,
182+ default = get_octopusvariable_quiet ('GitHub.App.PrivateKey' ) or get_octopusvariable_quiet (
183+ 'PreviewMerge.GitHub.App.PrivateKey' ))
117184 parser .add_argument ('--tenant-name' ,
118185 action = 'store' ,
119186 default = get_octopusvariable_quiet ('Octopus.Deployment.Tenant.Name' ))
@@ -239,6 +306,12 @@ def merge_changes(branch, new_repo, template_repo_name_url, new_repo_url):
239306
240307parser , _ = init_argparse ()
241308
309+ # The access token is generated from a github app or supplied directly as an access token
310+ token = generate_github_token (parser .github_app_id , parser .github_app_private_key ,
311+ parser .github_app_installation_id ) if len (
312+ parser .git_password .strip ()) == 0 else parser .git_password .strip ()
313+ username = 'x-access-token' if len (parser .git_username .strip ()) == 0 else parser .git_username .strip ()
314+
242315tenant_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .tenant_name .lower ())
243316new_project_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .new_project_name .lower ())
244317original_project_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .original_project_name .lower ())
@@ -249,21 +322,30 @@ def merge_changes(branch, new_repo, template_repo_name_url, new_repo_url):
249322branch = 'main'
250323
251324new_repo_url = parser .git_protocol + '://' + parser .git_host + '/' + parser .git_organization + '/' + new_repo + '.git'
252- new_repo_url_wth_creds = parser .git_protocol + '://' + parser . git_username + ':' + parser . git_password + '@' + \
325+ new_repo_url_wth_creds = parser .git_protocol + '://' + token + ':' + token + '@' + \
253326 parser .git_host + '/' + parser .git_organization + '/' + new_repo + '.git'
254327template_repo_name_url = parser .git_protocol + '://' + parser .git_host + '/' + parser .git_organization + '/' + \
255328 parser .template_repo_name + '.git'
256- template_repo_name_url_with_creds = parser .git_protocol + '://' + parser . git_username + ':' + \
257- parser . git_password + '@' + parser .git_host + '/' + \
329+ template_repo_name_url_with_creds = parser .git_protocol + '://' + token + ':' + \
330+ token + '@' + parser .git_host + '/' + \
258331 parser .git_organization + '/' + parser .template_repo_name + '.git'
259332
260- if not check_repo_exists (new_repo_url , parser .git_username , parser .git_password ):
261- print ('Downstream repo ' + new_repo_url + ' is not available' )
262- sys .exit (1 )
333+ if parser .git_host == 'github.com' :
334+ if not check_github_repo_exists (parser .git_organization , new_repo , username , token ):
335+ print ('Downstream repo ' + new_repo_url + ' is not available' )
336+ sys .exit (1 )
337+
338+ if not check_github_repo_exists (parser .git_organization , parser .template_repo_name , username , token ):
339+ print ('Upstream repo ' + template_repo_name_url + ' is not available' )
340+ sys .exit (1 )
341+ else :
342+ if not check_repo_exists (parser .git_protocol , parser .git_host , parser .git_organization , new_repo , username , token ):
343+ print ('Downstream repo ' + new_repo_url + ' is not available' )
344+ sys .exit (1 )
263345
264- if not check_repo_exists (template_repo_name_url , parser .git_username , parser .git_password ):
265- print ('Upstream repo ' + template_repo_name_url + ' is not available' )
266- sys .exit (1 )
346+ if not check_repo_exists (parser . git_protocol , parser .git_host , parser .git_organization , parser . template_repo_name , username , token ):
347+ print ('Upstream repo ' + template_repo_name_url + ' is not available' )
348+ sys .exit (1 )
267349
268350set_git_user ()
269351template_dir = clone_repo (template_repo_name_url , branch )
0 commit comments