Octopus.Script exported 2023-10-17 by mcasperson belongs to ‘GitHub’ category.
This step creates a new GitHub repository if it does not exist.
Parameters
When steps based on the template are included in a project’s deployment process, the parameters below can be set.
Repository Name
CreateGithubRepo.Git.Url.NewRepoName = #{Octopus.Project.Name}
The name of the new GitHub repository.
Repository Name Prefix
CreateGithubRepo.Git.Url.NewRepoNamePrefix =
An optional prefix to apply to the name of the new repository. The repository name will often be generated from the project name, and the prefix will be based on a tenant name (#{Octopus.Deployment.Tenant.Name}
) to ensure each tenant has a unique repo.
This value can be left blank.
GitHub Owner or Organization
CreateGithubRepo.Git.Url.Organization =
This is the GitHub owner or organisation where the new repo is created.
GitHub Access Token
CreateGithubRepo.Git.Credentials.AccessToken =
The access token used to authenticate with GitHub. See the GitHub documentation for more details.
Script body
Steps based on this template will execute the following Python script.
# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid
# having to use a regular user account.
import subprocess
import sys
# Install our own dependencies
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'])
import json
import subprocess
import sys
import os
import urllib.request
import base64
import re
import jwt
import time
import argparse
import platform
from urllib.request import urlretrieve
# If this script is not being run as part of an Octopus step, setting variables is a noop
if 'set_octopusvariable' not in globals():
def set_octopusvariable(variable, value):
pass
# If this script is not being run as part of an Octopus step, return variables from environment variables.
# Periods are replaced with underscores, and the variable name is converted to uppercase
if "get_octopusvariable" not in globals():
def get_octopusvariable(variable):
return os.environ[re.sub('\\.', '_', variable.upper())]
# If this script is not being run as part of an Octopus step, print directly to std out.
if 'printverbose' not in globals():
def printverbose(msg):
print(msg)
def printverbose_noansi(output):
"""
Strip ANSI color codes and print the output as verbose
:param output: The output to print
"""
output_no_ansi = re.sub(r'\x1b\[[0-9;]*m', '', output)
printverbose(output_no_ansi)
def get_octopusvariable_quiet(variable):
"""
Gets an octopus variable, or an empty string if it does not exist.
:param variable: The variable name
:return: The variable value, or an empty string if the variable does not exist
"""
try:
return get_octopusvariable(variable)
except:
return ''
def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,
append_to_path=None):
"""
The execute method provides the ability to execute external processes while capturing and returning the
output to std err and std out and exit code.
"""
my_env = os.environ.copy() if env is None else env
if append_to_path is not None:
my_env["PATH"] = append_to_path + os.pathsep + my_env['PATH']
process = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=open(os.devnull),
text=True,
cwd=cwd,
env=my_env)
stdout, stderr = process.communicate()
retcode = process.returncode
if not retcode == 0 and raise_on_non_zero:
raise Exception('command returned exit code ' + retcode)
if print_args is not None:
print_output(' '.join(args))
if print_output is not None:
print_output(stdout)
print_output(stderr)
return stdout, stderr, retcode
def init_argparse():
parser = argparse.ArgumentParser(
usage='%(prog)s [OPTION]',
description='Create a GitHub repo'
)
parser.add_argument('--new-repo-name', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.Git.Url.NewRepoName') or get_octopusvariable_quiet(
'Git.Url.NewRepoName') or get_octopusvariable_quiet('Octopus.Project.Name'))
parser.add_argument('--new-repo-name-prefix', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.Git.Url.NewRepoNamePrefix') or get_octopusvariable_quiet(
'Git.Url.NewRepoNamePrefix'))
parser.add_argument('--git-organization', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet(
'Git.Url.Organization'))
parser.add_argument('--github-app-id', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))
parser.add_argument('--github-app-installation-id', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet(
'GitHub.App.InstallationId'))
parser.add_argument('--github-app-private-key', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet(
'GitHub.App.PrivateKey'))
parser.add_argument('--github-access-token', action='store',
default=get_octopusvariable_quiet(
'CreateGithubRepo.Git.Credentials.AccessToken') or get_octopusvariable_quiet(
'Git.Credentials.AccessToken'),
help='The git password')
return parser.parse_known_args()
def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):
# Generate the tokens used by git and the GitHub API
app_id = github_app_id
signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))
payload = {
# Issued at time
'iat': int(time.time()),
# JWT expiration time (10 minutes maximum)
'exp': int(time.time()) + 600,
# GitHub App's identifier
'iss': app_id
}
# Create JWT
jwt_instance = jwt.JWT()
encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')
# Create access token
url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'
headers = {
'Authorization': 'Bearer ' + encoded_jwt,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
request = urllib.request.Request(url, headers=headers, method='POST')
response = urllib.request.urlopen(request)
response_json = json.loads(response.read().decode())
return response_json['token']
def generate_auth_header(token):
auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))
return 'Basic ' + auth.decode('ascii')
def verify_new_repo(token, cac_org, new_repo):
# Attempt to view the new repo
try:
url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo
headers = {
'Accept': 'application/vnd.github+json',
'Authorization': 'Bearer ' + token,
'X-GitHub-Api-Version': '2022-11-28'
}
request = urllib.request.Request(url, headers=headers)
urllib.request.urlopen(request)
return True
except:
return False
def create_new_repo(token, cac_org, new_repo):
# If we could not view the repo, assume it needs to be created.
# https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository
# Note you have to use the token rather than the JWT:
# https://stackoverflow.com/questions/39600396/bad-credentails-for-jwt-for-github-integrations-api
headers = {
'Authorization': 'token ' + token,
'Content-Type': 'application/json',
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
}
try:
# First try to create an organization repo:
# https://docs.github.com/en/free-pro-team@latest/rest/repos/repos#create-an-organization-repository
url = 'https://api.github.com/orgs/' + cac_org + '/repos'
body = {'name': new_repo}
request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8'))
urllib.request.urlopen(request)
except urllib.error.URLError as ex:
# Then fall back to creating a repo for the user:
# https://docs.github.com/en/free-pro-team@latest/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-for-the-authenticated-user
if ex.code == 404:
url = 'https://api.github.com/user/repos'
body = {'name': new_repo}
request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8'))
urllib.request.urlopen(request)
else:
raise ValueError("Failed to create thew new repository. This could indicate bad credentials.") from ex
def is_windows():
return platform.system() == 'Windows'
parser, _ = init_argparse()
if not parser.github_access_token.strip() and not (
parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):
print("You must supply the GitHub token, or the GitHub App ID and private key and installation ID")
sys.exit(1)
if not parser.new_repo_name.strip():
print("You must define the new repo name")
sys.exit(1)
# The access token is generated from a github app or supplied directly as an access token
token = generate_github_token(parser.github_app_id, parser.github_app_private_key, parser.github_app_installation_id) \
if not parser.github_access_token.strip() else parser.github_access_token.strip()
cac_org = parser.git_organization.strip()
new_repo_custom_prefix = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name_prefix.strip())
project_repo_sanitized = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name.strip())
# The prefix is optional
new_repo_prefix_with_separator = new_repo_custom_prefix + '_' if new_repo_custom_prefix else ''
# The new repo name is the prefix + the name of thew new project
new_repo = new_repo_prefix_with_separator + project_repo_sanitized
# This is the value of the forked git repo
set_octopusvariable('NewRepoUrl', 'https://github.com/' + cac_org + '/' + new_repo)
set_octopusvariable('NewRepo', new_repo)
if not verify_new_repo(token, cac_org, new_repo):
create_new_repo(token, cac_org, new_repo)
print(
'New repo was created at https://github.com/' + cac_org + '/' + new_repo)
else:
print('Repo at https://github.com/' + cac_org + '/' + new_repo + ' already exists and has not been modified')
print('New repo URL is defined in the output variable "NewRepoUrl": #{Octopus.Action[' +
get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepoUrl}')
print('New repo name is defined in the output variable "NewRepo": #{Octopus.Action[' +
get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepo}')
Provided under the Apache License version 2.0.
To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.
{
"Id": "493fa039-fd5c-47d2-b830-63cc32a19d04",
"Name": "GitHub - Create Repository",
"Description": "This step creates a new GitHub repository if it does not exist.",
"Version": 1,
"ExportedAt": "2023-10-17T02:12:09.605Z",
"ActionType": "Octopus.Script",
"Author": "mcasperson",
"Packages": [],
"Parameters": [
{
"Id": "5845b0e2-d679-4cec-9022-c233031e6b35",
"Name": "CreateGithubRepo.Git.Url.NewRepoName",
"Label": "Repository Name",
"HelpText": "The name of the new GitHub repository.",
"DefaultValue": "#{Octopus.Project.Name}",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "b85e4a76-bc9c-4a51-8ac0-653991824db2",
"Name": "CreateGithubRepo.Git.Url.NewRepoNamePrefix",
"Label": "Repository Name Prefix",
"HelpText": "An optional prefix to apply to the name of the new repository. The repository name will often be generated from the project name, and the prefix will be based on a tenant name (`#{Octopus.Deployment.Tenant.Name}`) to ensure each tenant has a unique repo.\n\n\nThis value can be left blank.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "b0d86188-fbb8-475e-89de-4b994170f2b7",
"Name": "CreateGithubRepo.Git.Url.Organization",
"Label": "GitHub Owner or Organization",
"HelpText": "This is the GitHub owner or organisation where the new repo is created.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "0ffd12dd-59ce-401a-b1df-cf3d6711e2eb",
"Name": "CreateGithubRepo.Git.Credentials.AccessToken",
"Label": "GitHub Access Token",
"HelpText": "The access token used to authenticate with GitHub. See the [GitHub documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) for more details.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "Sensitive"
}
}
],
"Properties": {
"Octopus.Action.RunOnServer": "true",
"Octopus.Action.Script.ScriptSource": "Inline",
"Octopus.Action.Script.Syntax": "Python",
"Octopus.Action.Script.ScriptBody": "# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\n# having to use a regular user account.\nimport subprocess\nimport sys\n\n# Install our own dependencies\nsubprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'])\n\nimport json\nimport subprocess\nimport sys\nimport os\nimport urllib.request\nimport base64\nimport re\nimport jwt\nimport time\nimport argparse\nimport platform\nfrom urllib.request import urlretrieve\n\n# If this script is not being run as part of an Octopus step, setting variables is a noop\nif 'set_octopusvariable' not in globals():\n def set_octopusvariable(variable, value):\n pass\n\n# If this script is not being run as part of an Octopus step, return variables from environment variables.\n# Periods are replaced with underscores, and the variable name is converted to uppercase\nif \"get_octopusvariable\" not in globals():\n def get_octopusvariable(variable):\n return os.environ[re.sub('\\\\.', '_', variable.upper())]\n\n# If this script is not being run as part of an Octopus step, print directly to std out.\nif 'printverbose' not in globals():\n def printverbose(msg):\n print(msg)\n\n\ndef printverbose_noansi(output):\n \"\"\"\n Strip ANSI color codes and print the output as verbose\n :param output: The output to print\n \"\"\"\n output_no_ansi = re.sub(r'\\x1b\\[[0-9;]*m', '', output)\n printverbose(output_no_ansi)\n\n\ndef get_octopusvariable_quiet(variable):\n \"\"\"\n Gets an octopus variable, or an empty string if it does not exist.\n :param variable: The variable name\n :return: The variable value, or an empty string if the variable does not exist\n \"\"\"\n try:\n return get_octopusvariable(variable)\n except:\n return ''\n\n\ndef execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\n append_to_path=None):\n \"\"\"\n The execute method provides the ability to execute external processes while capturing and returning the\n output to std err and std out and exit code.\n \"\"\"\n\n my_env = os.environ.copy() if env is None else env\n\n if append_to_path is not None:\n my_env[\"PATH\"] = append_to_path + os.pathsep + my_env['PATH']\n\n process = subprocess.Popen(args,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n stdin=open(os.devnull),\n text=True,\n cwd=cwd,\n env=my_env)\n stdout, stderr = process.communicate()\n retcode = process.returncode\n\n if not retcode == 0 and raise_on_non_zero:\n raise Exception('command returned exit code ' + retcode)\n\n if print_args is not None:\n print_output(' '.join(args))\n\n if print_output is not None:\n print_output(stdout)\n print_output(stderr)\n\n return stdout, stderr, retcode\n\n\ndef init_argparse():\n parser = argparse.ArgumentParser(\n usage='%(prog)s [OPTION]',\n description='Create a GitHub repo'\n )\n parser.add_argument('--new-repo-name', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.Git.Url.NewRepoName') or get_octopusvariable_quiet(\n 'Git.Url.NewRepoName') or get_octopusvariable_quiet('Octopus.Project.Name'))\n parser.add_argument('--new-repo-name-prefix', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.Git.Url.NewRepoNamePrefix') or get_octopusvariable_quiet(\n 'Git.Url.NewRepoNamePrefix'))\n parser.add_argument('--git-organization', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet(\n 'Git.Url.Organization'))\n parser.add_argument('--github-app-id', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\n parser.add_argument('--github-app-installation-id', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet(\n 'GitHub.App.InstallationId'))\n parser.add_argument('--github-app-private-key', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\n 'GitHub.App.PrivateKey'))\n parser.add_argument('--github-access-token', action='store',\n default=get_octopusvariable_quiet(\n 'CreateGithubRepo.Git.Credentials.AccessToken') or get_octopusvariable_quiet(\n 'Git.Credentials.AccessToken'),\n help='The git password')\n\n return parser.parse_known_args()\n\n\ndef generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\n # Generate the tokens used by git and the GitHub API\n app_id = github_app_id\n signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\n\n payload = {\n # Issued at time\n 'iat': int(time.time()),\n # JWT expiration time (10 minutes maximum)\n 'exp': int(time.time()) + 600,\n # GitHub App's identifier\n 'iss': app_id\n }\n\n # Create JWT\n jwt_instance = jwt.JWT()\n encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\n\n # Create access token\n url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\n headers = {\n 'Authorization': 'Bearer ' + encoded_jwt,\n 'Accept': 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28'\n }\n request = urllib.request.Request(url, headers=headers, method='POST')\n response = urllib.request.urlopen(request)\n response_json = json.loads(response.read().decode())\n return response_json['token']\n\n\ndef generate_auth_header(token):\n auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\n return 'Basic ' + auth.decode('ascii')\n\n\ndef verify_new_repo(token, cac_org, new_repo):\n # Attempt to view the new repo\n try:\n url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\n headers = {\n 'Accept': 'application/vnd.github+json',\n 'Authorization': 'Bearer ' + token,\n 'X-GitHub-Api-Version': '2022-11-28'\n }\n request = urllib.request.Request(url, headers=headers)\n urllib.request.urlopen(request)\n return True\n except:\n return False\n\n\ndef create_new_repo(token, cac_org, new_repo):\n # If we could not view the repo, assume it needs to be created.\n # https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository\n # Note you have to use the token rather than the JWT:\n # https://stackoverflow.com/questions/39600396/bad-credentails-for-jwt-for-github-integrations-api\n\n headers = {\n 'Authorization': 'token ' + token,\n 'Content-Type': 'application/json',\n 'Accept': 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n }\n\n try:\n # First try to create an organization repo:\n # https://docs.github.com/en/free-pro-team@latest/rest/repos/repos#create-an-organization-repository\n url = 'https://api.github.com/orgs/' + cac_org + '/repos'\n body = {'name': new_repo}\n request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8'))\n urllib.request.urlopen(request)\n except urllib.error.URLError as ex:\n # Then fall back to creating a repo for the user:\n # https://docs.github.com/en/free-pro-team@latest/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-for-the-authenticated-user\n if ex.code == 404:\n url = 'https://api.github.com/user/repos'\n body = {'name': new_repo}\n request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8'))\n urllib.request.urlopen(request)\n else:\n raise ValueError(\"Failed to create thew new repository. This could indicate bad credentials.\") from ex\n\n\ndef is_windows():\n return platform.system() == 'Windows'\n\n\nparser, _ = init_argparse()\n\nif not parser.github_access_token.strip() and not (\n parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\n print(\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\")\n sys.exit(1)\n\nif not parser.new_repo_name.strip():\n print(\"You must define the new repo name\")\n sys.exit(1)\n\n# The access token is generated from a github app or supplied directly as an access token\ntoken = generate_github_token(parser.github_app_id, parser.github_app_private_key, parser.github_app_installation_id) \\\n if not parser.github_access_token.strip() else parser.github_access_token.strip()\n\ncac_org = parser.git_organization.strip()\nnew_repo_custom_prefix = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name_prefix.strip())\nproject_repo_sanitized = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name.strip())\n\n# The prefix is optional\nnew_repo_prefix_with_separator = new_repo_custom_prefix + '_' if new_repo_custom_prefix else ''\n\n# The new repo name is the prefix + the name of thew new project\nnew_repo = new_repo_prefix_with_separator + project_repo_sanitized\n\n# This is the value of the forked git repo\nset_octopusvariable('NewRepoUrl', 'https://github.com/' + cac_org + '/' + new_repo)\nset_octopusvariable('NewRepo', new_repo)\n\nif not verify_new_repo(token, cac_org, new_repo):\n create_new_repo(token, cac_org, new_repo)\n print(\n 'New repo was created at https://github.com/' + cac_org + '/' + new_repo)\nelse:\n print('Repo at https://github.com/' + cac_org + '/' + new_repo + ' already exists and has not been modified')\n\nprint('New repo URL is defined in the output variable \"NewRepoUrl\": #{Octopus.Action[' +\n get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepoUrl}')\nprint('New repo name is defined in the output variable \"NewRepo\": #{Octopus.Action[' +\n get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepo}')\n"
},
"Category": "GitHub",
"HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/github-create-repo.json",
"Website": "/step-templates/493fa039-fd5c-47d2-b830-63cc32a19d04",
"Logo": "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",
"$Meta": {
"Type": "ActionTemplate"
}
}
Page updated on Tuesday, October 17, 2023