Skip to content

Commit ac69b29

Browse files
Mark R. Tuttlemarkrtuttle
authored andcommitted
Add automation of package release
1 parent 29d11cc commit ac69b29

File tree

4 files changed

+385
-0
lines changed

4 files changed

+385
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python3
2+
3+
"""Create a GitHub release for a tagged commit."""
4+
5+
import logging
6+
import os
7+
import re
8+
9+
import github_release
10+
11+
################################################################
12+
13+
def extend_argument_parser(parser):
14+
"""Add flags to the command line parser."""
15+
16+
parser.add_argument(
17+
'--source-package',
18+
help='Path to pip source installation package.'
19+
)
20+
21+
parser.add_argument(
22+
'--binary-package',
23+
required=True,
24+
help='Path to pip binary installation package.'
25+
)
26+
27+
parser.add_argument(
28+
'--tag',
29+
required=True,
30+
help='GitHub tag of the package being released'
31+
)
32+
33+
parser.add_argument(
34+
'--version',
35+
required=True,
36+
help='Version of the package being released'
37+
)
38+
39+
return parser
40+
41+
def main():
42+
"""Create a GitHub release for a tagged commit."""
43+
44+
parser = github_release.argument_parser()
45+
parser = extend_argument_parser(parser)
46+
args = parser.parse_args()
47+
args = github_release.argument_defaults(args)
48+
49+
if not os.path.isfile(args.binary_package):
50+
raise UserWarning("Binary package does not exist: {}"
51+
.format(args.binary_package))
52+
53+
if not re.match(r'[\w.-]+', args.tag, re.ASCII):
54+
raise UserWarning("Package version contains an illegal character: {}"
55+
.format(args.tag))
56+
57+
token = os.environ.get('GITHUB_TOKEN')
58+
if not token:
59+
logging.info(
60+
'Failed to find GitHub token in environment variable '
61+
'GITHUB_TOKEN'
62+
)
63+
64+
repo_name = os.environ.get('GITHUB_REPOSITORY')
65+
if not repo_name:
66+
logging.info(
67+
'Failed to find GitHub repository in environment variable '
68+
'GITHUB_REPOSITORY'
69+
)
70+
71+
repo = github_release.get_repository(repo_name, token)
72+
assets = [
73+
{
74+
'path': args.binary_package,
75+
'name': os.path.basename(args.binary_package),
76+
'label': 'PIP installation package',
77+
'type': 'application/binary'
78+
}
79+
]
80+
github_release.tagged_software_release(repo, args.tag, args.version,
81+
assets)
82+
83+
if __name__ == "__main__":
84+
main()
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env python3
2+
3+
"""Manage GitHub software releases."""
4+
5+
6+
import argparse
7+
import logging
8+
import os
9+
10+
import github
11+
12+
NIGHTLY_TAG = 'nightly'
13+
NIGHTLY_BRANCH = 'master'
14+
15+
TAGGED_RELEASE_MESSAGE = "tagged_release_message.md"
16+
17+
################################################################
18+
19+
def argument_parser():
20+
"""Parser for command-line arguments."""
21+
22+
parser = argparse.ArgumentParser(
23+
description='Create GitHub software release with build artifacts.'
24+
)
25+
parser.add_argument(
26+
'--verbose',
27+
action='store_true',
28+
help='Verbose output.'
29+
)
30+
parser.add_argument(
31+
'--debug',
32+
action='store_true',
33+
help='Debugging output.'
34+
)
35+
36+
return parser
37+
38+
def argument_defaults(args):
39+
"""Default values for command-line arguments."""
40+
41+
# Configure logging to print INFO messages by default
42+
args.verbose = True
43+
if args.debug:
44+
logging.basicConfig(level=logging.DEBUG,
45+
format='%(levelname)s: %(message)s')
46+
elif args.verbose:
47+
logging.basicConfig(level=logging.INFO,
48+
format='%(levelname)s: %(message)s')
49+
else:
50+
logging.basicConfig(format='%(levelname)s: %(message)s')
51+
52+
return args
53+
54+
################################################################
55+
56+
def get_repository(full_name, token):
57+
"""GitHub repository with given name authenticated with given token."""
58+
59+
return github.Github(token).get_repo(full_name)
60+
61+
def lookup_tag(repo, tag_name):
62+
"""The reference corresponding to a tag name."""
63+
64+
try:
65+
ref = repo.get_git_ref('tags/{}'.format(tag_name))
66+
except github.UnknownObjectException as error:
67+
logging.info('Failed to find reference for tag %s: %s', tag_name, error)
68+
return None
69+
70+
logging.info('Found reference for tag %s: %s', tag_name, ref)
71+
return ref
72+
73+
def lookup_release(repo, tag_name):
74+
"""The release corresponding to a tag name."""
75+
76+
for release in repo.get_releases():
77+
if release.tag_name == tag_name:
78+
logging.info('Found release for tag %s: %s', tag_name, release)
79+
return release
80+
logging.info('Failed to find release for tag %s', tag_name)
81+
return None
82+
83+
################################################################
84+
# Create a GitHub release for a versioned software release (a tagged
85+
# commit) with a set of installation packages for this version.
86+
87+
def tagged_release_message(version, path=None):
88+
"""The message to use with a tagged release."""
89+
90+
path = path or TAGGED_RELEASE_MESSAGE
91+
path = os.path.join(os.path.dirname(__file__), path)
92+
93+
try:
94+
with open(path) as msg:
95+
return msg.read().format(version)
96+
except FileNotFoundError:
97+
logging.info("Couldn't open tagged release message file: %s", path)
98+
return "This is release {}".format(version)
99+
100+
def create_tagged_release(repo, tag_name, version=None):
101+
"""Create a release for a tagged commit."""
102+
103+
release = lookup_release(repo, tag_name)
104+
if release:
105+
release.delete_release()
106+
logging.info('Deleted release for tag %s: %s', tag_name, release)
107+
108+
reference = lookup_tag(repo, tag_name)
109+
if not reference:
110+
raise UserWarning("Tag does not exist: {}".format(tag_name))
111+
112+
version = version or tag_name.split('-')[-1]
113+
release_name = tag_name
114+
release_msg = tagged_release_message(version)
115+
release = repo.create_git_release(tag_name, release_name, release_msg)
116+
logging.info('Created release for tag %s: %s', tag_name, release)
117+
if not release:
118+
raise UserWarning("Failed to create tagged release for tag {}"
119+
.format(tag_name))
120+
121+
return release
122+
123+
def upload_release_asset(release, path,
124+
label=None, content_type=None, name=None):
125+
"""Upload an asset (an installation package) to a release."""
126+
127+
filename = os.path.basename(path)
128+
label = label or filename
129+
name = name or filename
130+
content_type = content_type or 'text/plain'
131+
132+
try:
133+
asset = release.upload_asset(path, label, content_type, name)
134+
except github.GithubException as error:
135+
logging.info("Failed to upload asset '%s': %s", path, error)
136+
137+
logging.info("Uploaded asset '%s': %s", path, asset)
138+
return asset
139+
140+
def tagged_software_release(repo, tag_name, version=None, assets=None):
141+
"""Create a release for a tagged commit with a list of assets."""
142+
143+
# asset: {
144+
# path : required string: local path to the assest to upload to GitHub
145+
# name : string: filename to use for the asset on GitHub
146+
# label : string: text to display in the link to the asset in release
147+
# type : string content type of the asset
148+
# }
149+
assets = assets or []
150+
151+
release = create_tagged_release(repo, tag_name, version)
152+
for asset in assets:
153+
upload_release_asset(release, asset['path'],
154+
label=asset.get('label'),
155+
content_type=asset.get('type'),
156+
name=asset.get('name'))
157+
158+
################################################################
159+
# Create a GitHub release for a nightly build of a development branch
160+
# with a set of installation packages for the nightly build.
161+
#
162+
# This works by creating (updating) a 'nightly' tag for the tip of the
163+
# development branch, then creating an ordinary tagged release for
164+
# this newly tagged commit, but a) the release message tailored to a
165+
# nightly release, and b) the release type is set to "prerelease".
166+
#
167+
# One issue with this implementation is that the constant updating of
168+
# the 'nightly' tag will require constant forced pulls to the local
169+
# copy of the 'nightly' tag. This annoyance is the primary reason
170+
# this implementation is not currently used.
171+
172+
def update_nightly_tag(repo):
173+
"""Update the nightly tag to the tip of the development branch."""
174+
175+
reference = lookup_tag(repo, NIGHTLY_TAG)
176+
if reference:
177+
reference.delete()
178+
logging.info('Deleted tag %s: %s', NIGHTLY_TAG, reference)
179+
180+
ref = 'refs/tags/{}'.format(NIGHTLY_TAG)
181+
sha = repo.get_branch(NIGHTLY_BRANCH).commit.sha
182+
reference = repo.create_git_ref(ref, sha)
183+
logging.info('Created tag %s for branch %s (ref %s, sha %s): %s',
184+
NIGHTLY_TAG, NIGHTLY_BRANCH, ref, sha, reference)
185+
186+
def create_nightly_release(repo):
187+
"""Create a tagged release for the nightly commit."""
188+
189+
release = lookup_release(repo, NIGHTLY_TAG)
190+
if release:
191+
release.delete_release()
192+
logging.info('Deleted release for tag %s: %s', NIGHTLY_TAG, release)
193+
194+
update_nightly_tag(repo)
195+
reference = lookup_tag(repo, NIGHTLY_TAG)
196+
if not reference:
197+
raise UserWarning("Tag does not exist: {}".format(NIGHTLY_TAG))
198+
199+
release_name = "Nightly release"
200+
release_msg = "This is a nightly release"
201+
# GitHub doesn't display release with draft=False, prelease=True
202+
release = repo.create_git_release(NIGHTLY_TAG,
203+
release_name, release_msg,
204+
prerelease=True)
205+
logging.info('Created nightly release with tag %s for branch %s: %s',
206+
NIGHTLY_TAG, NIGHTLY_BRANCH, release)
207+
if not release:
208+
raise UserWarning(
209+
"Failed to create nightly release with tag {} for branch {}"
210+
.format(NIGHTLY_TAG, NIGHTLY_BRANCH))
211+
return release
212+
213+
def nightly_software_release(repo, assets=None):
214+
"""Create a tagged release for the nightly commit and pacakges."""
215+
216+
# asset: {
217+
# path : required string: local path to the assest to upload to GitHub
218+
# name : string: filename to use for the asset on GitHub
219+
# label : string: text to display in the link to the asset in release
220+
# type : string: content type of the asset
221+
# }
222+
assets = assets or []
223+
224+
release = create_nightly_release(repo)
225+
for asset in assets:
226+
upload_release_asset(release, asset['path'],
227+
label=asset.get('label'),
228+
content_type=asset.get('type'),
229+
name=asset.get('name'))
230+
231+
################################################################

.github/workflows/release.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: CBMC viewer release
2+
3+
# A new release is triggered by a new tag of the form viewer-VERSION
4+
on:
5+
push:
6+
tags:
7+
- viewer-*
8+
9+
jobs:
10+
Release:
11+
name: CBMC viewer release
12+
runs-on: ubuntu-18.04
13+
14+
steps:
15+
16+
- name: Checkout the repository
17+
uses: actions/checkout@v2
18+
19+
- name: Set the package version
20+
run: |
21+
# The environment variable GITHUB_REF is refs/tags/viewer-*
22+
echo "::set-env name=TAG::${GITHUB_REF:10}"
23+
echo "::set-env name=VERSION::${GITHUB_REF:17}"
24+
printenv | sort
25+
cat ${GITHUB_EVENT_PATH}
26+
27+
- name: Create the package
28+
run: |
29+
# Patch the version number the source to be consistent with the tag
30+
sed -i.bak \
31+
"s/NUMBER *=.*/NUMBER = \"${VERSION}\"/" \
32+
cbmc_viewer/version.py
33+
34+
# Patch the version number pip setup to be consistent with the tag
35+
sed -i.bak \
36+
"s/version *=.*/version = \"${VERSION}\",/" \
37+
setup.py
38+
39+
# Create the package
40+
python3 -m pip install --upgrade setuptools wheel
41+
make pip
42+
43+
# Record the package name
44+
# The source *.zip and binary *.whl packages are in dist
45+
echo "::set-env name=PACKAGE::$(ls dist/*.whl)"
46+
47+
- name: Create the release
48+
env:
49+
# GitHub creates this secret for authentication in a workflow
50+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
run: |
52+
python3 -m pip install --upgrade pygithub
53+
python3 .github/workflows/create_tagged_release.py --verbose \
54+
--binary-package ${PACKAGE} \
55+
--tag ${TAG} \
56+
--version ${VERSION}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
This is the CBMC Viewer version {}.
2+
3+
To install this release, download the "PIP installation package" as $PACKAGE and run
4+
```
5+
sudo python3 -m pip install --upgrade $PACKAGE
6+
```
7+
8+
9+
To get the best results, install Exuberant Ctags with the following commands:
10+
```
11+
On MacOS: brew install ctags
12+
On Ubuntu: sudo apt-get install ctags
13+
On Windows: Download from https://sourceforge.net/projects/ctags
14+
```

0 commit comments

Comments
 (0)