Skip to content

Instantly share code, notes, and snippets.

@mcguffin
Last active July 6, 2023 04:38
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mcguffin/7fbe709a624f672f60c770c080cbc41a to your computer and use it in GitHub Desktop.
Save mcguffin/7fbe709a624f672f60c770c080cbc41a to your computer and use it in GitHub Desktop.
Python script to create GitHub release
#!/usr/bin/env python3
import argparse, glob, json, re, subprocess, urllib.request, os, sys
class version_number:
major=0
minor=0
release=0
release_suffix=''
def __init__(self,version='0.0.0'):
(self.major,self.minor,release) = version.split('.',2)
# extract release suffix like 0.1.2-alpha1234
res = re.search(r'[^0-9]',release)
if res:
s = res.start()
self.release = release[:s]
self.release_suffix = release[s:]
else:
self.release = release
(self.major,self.minor,self.release) = map(int,(self.major,self.minor,self.release))
def __add__(self,other):
self.release_suffix = ''
self.major += other.major
self.minor += other.minor
self.release += other.release
return self
def incr(self,what="release"):
self.release_suffix = ''
if what == 'release':
self.release+=1
if what == 'minor':
self.minor+=1
self.release = 0
if what == 'major':
self.major+=1
self.minor = 0
self.release = 0
return self
def __str__(self):
s = '.'.join( map(str,(self.major,self.minor,self.release)) )
return '%s%s' % (s,self.release_suffix)
def shell(cmd, check=True):
result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, check=check )
return result.stdout.decode('utf-8').strip()
def get_file_data( plugin_file, headers={'Plugin Name':'Plugin Name','Version':'Version'} ):
f = open(plugin_file,'r')
contents = f.read()
f.close()
contents.replace("\r","\n")
ret = {}
for field,regex in headers.items():
res = re.compile( '^[ \t*#@]*%s:(?P<value>.*)$' % (regex), re.I | re.M ).search(contents)
ret[field] = res.group('value').strip()
#
#contents.match()
return ret
def update_plugin_version( new_version ):
for plugin_file in glob.glob("*.php"):
fdata = get_file_data( plugin_file )
if 'Version' in fdata:
# read
f = open(plugin_file,'r')
contents = f.read()
f.close()
reg = re.compile( u'^([ \t*#@]*)Version:[\s\t]*(.*)$', re.I | re.M )
repl = r'\1Version: %s' % (new_version)
contents = reg.sub( repl, contents )
f = open(plugin_file,'w')
contents = f.write(contents)
f.close()
# add commit push
shell('git add .')
try:
shell('git commit -q -m "Release version %s"' % (new_version), check=True)
shell('git push', check=True)
return True
except subprocess.CalledProcessError:
return False
return False
# check env config
try:
access_token = shell('security find-generic-password -a ${USER} -s GithubAccessToken -w')
except KeyError:
print("Missing env var `GITHUB_ACCESS_TOKEN`")
sys.exit(1)
# check if git repo
repo_remote = shell('git config --get remote.origin.url')
if not repo_remote:
print("Not a git repository")
sys.exit(1)
repo_name = re.sub(r'\.git$','',os.path.basename(repo_remote) )
repo_owner = shell('git config --get user.name')
repo_current_branch = shell('git rev-parse --abbrev-ref HEAD')
# Parse cli args
parser = argparse.ArgumentParser()
parser.add_argument("-r","--release",
default='release',
const='release',
help="Type of Release. 'release' (default) will increment the last version number, 'minor' the middle one and 'major' the first one.",
type=str,
choices=['release','minor','major'],
nargs='?')
parser.add_argument("-v","--version", help="Version number. Must be in format `<major>.<minor>.<release>`. You may append something like `1.2.3-beta-RC3` also. For automatic versioning use -r.",type=str )
parser.add_argument("-m","--message", help="Release Message")
parser.add_argument("-p","--pre", type=bool, help="Prerelease", default=False)
parser.add_argument("-d","--draft", type=bool, help="Draft", default=False)
parser.add_argument("-b","--branch", type=str, help="Branch", default=repo_current_branch)
args = parser.parse_args()
# get version string
if args.version:
new_version = version_number(args.version)
elif args.release:
#
new_version = False
for plugin_file in glob.glob("*.php"):
fdata = get_file_data( plugin_file )
if 'Version' in fdata:
new_version = version_number(fdata['Version'])
break
else:
print("Nope...",plugin_file)
new_version.incr(args.release)
print('Building Release %s from branch %s now.' % (new_version,args.branch))
# get message
if args.message:
body = args.message
else:
body = "Release of version %s from branch %s" % (new_version, args.branch)
readme_data = get_file_data('readme.txt',headers={
'Requires at least': 'Requires at least',
'Tested up to': 'Tested up to',
'Requires PHP': 'Requires PHP'
})
body = """%s
Requires at least: %s
Tested up to: %s
Requires PHP: %s
""" % (body,
readme_data['Requires at least'],
readme_data['Tested up to'],
readme_data['Requires PHP']
)
# Update plugin header, commit, push
if not update_plugin_version(new_version):
print('Could not push changes')
sys.exit(1)
# create tag via api
request_data = {
'tag_name': "v%s" % (new_version),
'target_commitish': args.branch,
'name': "v%s" % (new_version),
'body': body,
'draft': bool(args.draft),
'prerelease': bool(args.pre)
}
request_url = 'https://api.github.com/repos/%s/%s/releases?access_token=%s' % (repo_owner,repo_name,access_token)
# send api request
print('Create tag...')
with urllib.request.urlopen(request_url,data=json.dumps(request_data).encode('utf-8')) as f:
# parse response
api_response = json.loads( f.read().decode('utf-8') )
# create installable zip
# git archive --format=zip -v --output=../<reponame>.zip --worktree-attributes HEAD
# mk zip
zip_path = '../%s.zip' % (repo_name)
print('Create installable ZIP...')
shell('git archive --format=zip --prefix=%s/ --output=%s --worktree-attributes HEAD' % ( repo_name, zip_path ) )
# upload zip to tag
print('Upload ZIP...')
upload_url = '%s?name=%s.zip' % ( re.sub( r'{\?.*}$', '', api_response['upload_url'] ), repo_name )
hdl = open('../%s.zip' % (repo_name),'rb')
# post file
upload_req = urllib.request.Request(upload_url,
data = hdl.read(),
headers = {
'Authorization' : 'token %s' % (access_token),
'Content-Type' : 'application/zip'
}
)
with urllib.request.urlopen( upload_req ) as f:
# parse response
upload_response = json.loads( f.read().decode('utf-8') )
print('Cleanup...')
shell('rm %s' % (zip_path) )
print('All done.')
@mcguffin
Copy link
Author

mcguffin commented Dec 2, 2017

Setup

  1. Move the script to some directory covered by your local $PATH variable.
  2. Go to your github account and create a presonal access token, granting access to the repo scope
  3. Exec security add-generic-password -a ${USER} -s GithubAccessToken -w <your access token> in the commandline.
  4. Invoke the script in the root directory of your github repo.

Usage
Your repository's remote origin must be a github location. Setup and commit a .gitattributes file to exclude developer related files like sources or tests.

$ git-release.py -h
usage: git-release.py [-h] [-r [{release,minor,major}]] [-v VERSION]
                      [-m MESSAGE] [-p PRE] [-d DRAFT] [-b BRANCH]

optional arguments:
  -h, --help            show this help message and exit
  -r [{release,minor,major}], --release [{release,minor,major}]
                        Type of Release. 'release' (default) will increment
                        the last version number, 'minor' the middle one and
                        'major' the first one.
  -v VERSION, --version VERSION
                        Version number. Must be in format
                        <major>.<minor>.<release>. For automatic versioning

                        use -r option instead.
  -m MESSAGE, --message MESSAGE
                        Release Message
  -p PRE, --pre PRE     Prerelease
  -d DRAFT, --draft DRAFT
                        Draft
  -b BRANCH, --branch BRANCH
                        Branch

@Anton-V-K
Copy link

Anton-V-K commented Nov 22, 2022

Thanks for sharing!
Will the script work for ordinary privatly hosted git-repositories?
I see the script has direct references to https://api.github.com/..., so probably it won't work as is.

@mcguffin
Copy link
Author

In theory it should work, as long as your github ssh key and token credentials are working.
FYI I havent looked into this gist for ages, and I'm using something else now to create my WordPress Plugin releases.

@dcorrea777
Copy link

Is this script only for osx?

@mcguffin
Copy link
Author

mcguffin commented Jul 5, 2023

@dcorrea777 It's calling the macOS password manager shell in line 100 – this is osx-only.
All the rest should be platform independant, as only the git command and rm is used.
With a hard-coded github access token you should be good to go.

@dcorrea777
Copy link

Got it @mcguffin

This line itself is giving me an error, because I am not using osx but linux, is there any step that I should disregard so that it does not execute this osx command line?

@mcguffin
Copy link
Author

mcguffin commented Jul 6, 2023

@dcorrea777 replace lines 99 to 103 with access_token = '<your github access token here>'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment