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 json
4
+ import subprocess
5
+ import sys
6
+
7
+ # Install our own dependencies
8
+ subprocess .check_call ([sys .executable , '-m' , 'pip' , 'install' , 'jwt' ])
9
+
1
10
# This script previews the changes to be merged in from an upstream repo. It makes use of the diff2html
2
11
# tool. Run this script in the octopussamples/diff2html container image, which has diff2html and Python 3
3
12
# installed and ready to use.
6
15
import subprocess
7
16
import sys
8
17
import os
18
+ import time
9
19
import urllib .request
10
20
import base64
11
21
import re
22
+ import jwt
12
23
13
24
# If this script is not being run as part of an Octopus step, createartifact is a noop
14
25
if "createartifact" not in globals ():
@@ -75,6 +86,36 @@ def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose
75
86
76
87
return stdout , stderr , retcode
77
88
89
+ def generate_github_token (github_app_id , github_app_private_key , github_app_installation_id ):
90
+ # Generate the tokens used by git and the GitHub API
91
+ app_id = github_app_id
92
+ signing_key = jwt .jwk_from_pem (github_app_private_key .encode ('utf-8' ))
93
+
94
+ payload = {
95
+ # Issued at time
96
+ 'iat' : int (time .time ()),
97
+ # JWT expiration time (10 minutes maximum)
98
+ 'exp' : int (time .time ()) + 600 ,
99
+ # GitHub App's identifier
100
+ 'iss' : app_id
101
+ }
102
+
103
+ # Create JWT
104
+ jwt_instance = jwt .JWT ()
105
+ encoded_jwt = jwt_instance .encode (payload , signing_key , alg = 'RS256' )
106
+
107
+ # Create access token
108
+ url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'
109
+ headers = {
110
+ 'Authorization' : 'Bearer ' + encoded_jwt ,
111
+ 'Accept' : 'application/vnd.github+json' ,
112
+ 'X-GitHub-Api-Version' : '2022-11-28'
113
+ }
114
+ request = urllib .request .Request (url , headers = headers , method = 'POST' )
115
+ response = urllib .request .urlopen (request )
116
+ response_json = json .loads (response .read ().decode ())
117
+ return response_json ['token' ]
118
+
78
119
79
120
def check_repo_exists (url , username , password ):
80
121
try :
@@ -122,6 +163,15 @@ def init_argparse():
122
163
action = 'store' ,
123
164
default = get_octopusvariable_quiet ('Git.Url.Organization' ) or get_octopusvariable_quiet (
124
165
'PreviewMerge.Git.Url.Organization' ))
166
+ parser .add_argument ('--github-app-id' , action = 'store' ,
167
+ default = get_octopusvariable_quiet ('GitHub.App.Id' ) or get_octopusvariable_quiet (
168
+ 'PreviewMerge.GitHub.App.Id' ))
169
+ parser .add_argument ('--github-app-installation-id' , action = 'store' ,
170
+ default = get_octopusvariable_quiet ('GitHub.App.InstallationId' ) or get_octopusvariable_quiet (
171
+ 'PreviewMerge.GitHub.App.InstallationId' ))
172
+ parser .add_argument ('--github-app-private-key' , action = 'store' ,
173
+ default = get_octopusvariable_quiet ('GitHub.App.PrivateKey' ) or get_octopusvariable_quiet (
174
+ 'PreviewMerge.GitHub.App.PrivateKey' ))
125
175
parser .add_argument ('--tenant-name' ,
126
176
action = 'store' ,
127
177
default = get_octopusvariable_quiet ('Octopus.Deployment.Tenant.Name' ))
@@ -143,6 +193,12 @@ def init_argparse():
143
193
144
194
parser , _ = init_argparse ()
145
195
196
+ # The access token is generated from a github app or supplied directly as an access token
197
+ token = generate_github_token (parser .github_app_id , parser .github_app_private_key ,
198
+ parser .github_app_installation_id ) if len (
199
+ parser .git_password .strip ()) == 0 else parser .git_password .strip ()
200
+ username = 'x-access-token' if len (parser .git_username .strip ()) == 0 else parser .git_username .strip ()
201
+
146
202
exit_code = 0 if parser .silent_fail else 1
147
203
tenant_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .tenant_name .lower ())
148
204
new_project_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .new_project_name .lower ())
@@ -154,19 +210,19 @@ def init_argparse():
154
210
branch = 'main'
155
211
156
212
new_repo_url = parser .git_protocol + '://' + parser .git_host + '/' + parser .git_organization + '/' + new_repo + '.git'
157
- new_repo_url_wth_creds = parser .git_protocol + '://' + parser . git_username + ':' + parser . git_password + '@' + \
213
+ new_repo_url_wth_creds = parser .git_protocol + '://' + username + ':' + token + '@' + \
158
214
parser .git_host + '/' + parser .git_organization + '/' + new_repo + '.git'
159
215
template_repo_name_url = parser .git_protocol + '://' + parser .git_host + '/' + parser .git_organization + '/' + \
160
216
parser .template_repo_name + '.git'
161
- template_repo_name_url_with_creds = parser .git_protocol + '://' + parser . git_username + ':' + \
162
- parser . git_password + '@' + parser .git_host + '/' + \
217
+ template_repo_name_url_with_creds = parser .git_protocol + '://' + username + ':' + \
218
+ token + '@' + parser .git_host + '/' + \
163
219
parser .git_organization + '/' + parser .template_repo_name + '.git'
164
220
165
- if not check_repo_exists (new_repo_url , parser . git_username , parser . git_password ):
221
+ if not check_repo_exists (new_repo_url , username , token ):
166
222
print ('Downstream repo ' + new_repo_url + ' is not available' )
167
223
sys .exit (exit_code )
168
224
169
- if not check_repo_exists (template_repo_name_url , parser . git_username , parser . git_password ):
225
+ if not check_repo_exists (template_repo_name_url , username , token ):
170
226
print ('Upstream repo ' + template_repo_name_url + ' is not available' )
171
227
sys .exit (exit_code )
172
228
0 commit comments