Skip to content

Commit 1c25eaa

Browse files
committed
Allow use of github app credentials
1 parent 686e389 commit 1c25eaa

File tree

1 file changed

+61
-5
lines changed

1 file changed

+61
-5
lines changed

management_instance/runbooks/shared_scripts/preview_merge_repo.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
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.
@@ -6,9 +15,11 @@
615
import subprocess
716
import sys
817
import os
18+
import time
919
import urllib.request
1020
import base64
1121
import re
22+
import jwt
1223

1324
# If this script is not being run as part of an Octopus step, createartifact is a noop
1425
if "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

79120
def 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

144194
parser, _ = 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+
146202
exit_code = 0 if parser.silent_fail else 1
147203
tenant_name_sanitized = re.sub('[^a-zA-Z0-9]', '_', parser.tenant_name.lower())
148204
new_project_name_sanitized = re.sub('[^a-zA-Z0-9]', '_', parser.new_project_name.lower())
@@ -154,19 +210,19 @@ def init_argparse():
154210
branch = 'main'
155211

156212
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 + '@' + \
158214
parser.git_host + '/' + parser.git_organization + '/' + new_repo + '.git'
159215
template_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

Comments
 (0)