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+
110# This script previews the changes to be merged in from an upstream repo. It makes use of the diff2html
211# tool. Run this script in the octopussamples/diff2html container image, which has diff2html and Python 3
312# installed and ready to use.
615import subprocess
716import sys
817import os
18+ import time
919import urllib .request
1020import base64
1121import re
22+ import jwt
1223
1324# If this script is not being run as part of an Octopus step, createartifact is a noop
1425if "createartifact" not in globals ():
@@ -75,6 +86,36 @@ def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose
7586
7687 return stdout , stderr , retcode
7788
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+
78119
79120def check_repo_exists (url , username , password ):
80121 try :
@@ -122,6 +163,15 @@ def init_argparse():
122163 action = 'store' ,
123164 default = get_octopusvariable_quiet ('Git.Url.Organization' ) or get_octopusvariable_quiet (
124165 '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' ))
125175 parser .add_argument ('--tenant-name' ,
126176 action = 'store' ,
127177 default = get_octopusvariable_quiet ('Octopus.Deployment.Tenant.Name' ))
@@ -143,6 +193,12 @@ def init_argparse():
143193
144194parser , _ = init_argparse ()
145195
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+
146202exit_code = 0 if parser .silent_fail else 1
147203tenant_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .tenant_name .lower ())
148204new_project_name_sanitized = re .sub ('[^a-zA-Z0-9]' , '_' , parser .new_project_name .lower ())
@@ -154,19 +210,19 @@ def init_argparse():
154210branch = 'main'
155211
156212new_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 + '@' + \
158214 parser .git_host + '/' + parser .git_organization + '/' + new_repo + '.git'
159215template_repo_name_url = parser .git_protocol + '://' + parser .git_host + '/' + parser .git_organization + '/' + \
160216 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 + '/' + \
163219 parser .git_organization + '/' + parser .template_repo_name + '.git'
164220
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 ):
166222 print ('Downstream repo ' + new_repo_url + ' is not available' )
167223 sys .exit (exit_code )
168224
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 ):
170226 print ('Upstream repo ' + template_repo_name_url + ' is not available' )
171227 sys .exit (exit_code )
172228
0 commit comments