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
+
1
10
import argparse
2
11
import subprocess
3
12
import sys
4
13
import os
5
14
import urllib .request
6
15
import base64
7
16
import re
17
+ import json
18
+ import jwt
8
19
9
20
# If this script is not being run as part of an Octopus step, print directly to std out.
10
21
if "printverbose" not in globals ():
@@ -67,7 +78,8 @@ def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose
67
78
return stdout , stderr , retcode
68
79
69
80
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'
71
83
try :
72
84
auth = base64 .b64encode ((username + ':' + password ).encode ('ascii' ))
73
85
auth_header = "Basic " + auth .decode ('ascii' )
@@ -81,6 +93,52 @@ def check_repo_exists(url, username, password):
81
93
return False
82
94
83
95
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
+
84
142
def init_argparse ():
85
143
parser = argparse .ArgumentParser (
86
144
usage = '%(prog)s [OPTION] [FILE]...' ,
@@ -114,6 +172,15 @@ def init_argparse():
114
172
action = 'store' ,
115
173
default = get_octopusvariable_quiet ('Git.Url.Organization' ) or get_octopusvariable_quiet (
116
174
'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' ))
117
184
parser .add_argument ('--tenant-name' ,
118
185
action = 'store' ,
119
186
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):
239
306
240
307
parser , _ = init_argparse ()
241
308
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
+
242
315
tenant_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .tenant_name .lower ())
243
316
new_project_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .new_project_name .lower ())
244
317
original_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):
249
322
branch = 'main'
250
323
251
324
new_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 + '@' + \
253
326
parser .git_host + '/' + parser .git_organization + '/' + new_repo + '.git'
254
327
template_repo_name_url = parser .git_protocol + '://' + parser .git_host + '/' + parser .git_organization + '/' + \
255
328
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 + '/' + \
258
331
parser .git_organization + '/' + parser .template_repo_name + '.git'
259
332
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 )
263
345
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 )
267
349
268
350
set_git_user ()
269
351
template_dir = clone_repo (template_repo_name_url , branch )
0 commit comments