GitHub - Create Repository

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.

Report an issue

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"
  }
}

History

Page updated on Tuesday, October 17, 2023