Skip to content

Commit 13a38ae

Browse files
committed
Allow merging of github repos
1 parent d73f7c1 commit 13a38ae

File tree

2 files changed

+93
-11
lines changed

2 files changed

+93
-11
lines changed

management_instance/runbooks/shared_scripts/merge_repo.py

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
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+
110
import argparse
211
import subprocess
312
import sys
413
import os
514
import urllib.request
615
import base64
716
import 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.
1021
if "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+
84142
def 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

240307
parser, _ = 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+
242315
tenant_name_sanitized = re.sub('[^a-zA-Z0-9]', '_', parser.tenant_name.lower())
243316
new_project_name_sanitized = re.sub('[^a-zA-Z0-9]', '_', parser.new_project_name.lower())
244317
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):
249322
branch = 'main'
250323

251324
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 + '@' + \
253326
parser.git_host + '/' + parser.git_organization + '/' + new_repo + '.git'
254327
template_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

268350
set_git_user()
269351
template_dir = clone_repo(template_repo_name_url, branch)

management_instance/runbooks/shared_scripts/preview_merge_repo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid
22
# having to use a regular user account.
3-
import json
43
import subprocess
54
import sys
65

@@ -20,6 +19,7 @@
2019
import base64
2120
import re
2221
import jwt
22+
import json
2323

2424
# If this script is not being run as part of an Octopus step, createartifact is a noop
2525
if "createartifact" not in globals():

0 commit comments

Comments
 (0)