Skip to content

Commit 14f6a2d

Browse files
committed
Add helper to publish files in GH releases
The upload is otherwise shaky. Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent 5671563 commit 14f6a2d

File tree

1 file changed

+204
-0
lines changed

1 file changed

+204
-0
lines changed

etc/scripts/publish_files.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright (c) nexB Inc. and others. All rights reserved.
4+
# ScanCode is a trademark of nexB Inc.
5+
# SPDX-License-Identifier: Apache-2.0
6+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
7+
# See https://github.com/nexB/scancode-toolkit for support or download.
8+
# See https://aboutcode.org for more information about nexB OSS projects.
9+
#
10+
import hashlib
11+
import os
12+
import sys
13+
14+
from pathlib import Path
15+
16+
import click
17+
import requests
18+
import utils_thirdparty
19+
20+
from github_release_retry import github_release_retry as grr
21+
22+
"""
23+
Create GitHub releases and upload files there.
24+
"""
25+
26+
27+
def get_files(location):
28+
"""
29+
Return an iterable of (filename, Path, md5) tuples for files in the `location`
30+
directory tree recursively.
31+
"""
32+
for top, _dirs, files in os.walk(location):
33+
for filename in files:
34+
pth = Path(os.path.join(top, filename))
35+
with open(pth, 'rb') as fi:
36+
md5 = hashlib.md5(fi.read()).hexdigest()
37+
yield filename, pth, md5
38+
39+
40+
def get_etag_md5(url):
41+
"""
42+
Return the cleaned etag of URL `url` or None.
43+
"""
44+
headers = utils_thirdparty.get_remote_headers(url)
45+
headers = {k.lower(): v for k, v in headers.items()}
46+
etag = headers .get('etag')
47+
if etag:
48+
etag = etag.strip('"').lower()
49+
return etag
50+
51+
52+
def create_or_update_release_and_upload_directory(
53+
user,
54+
repo,
55+
tag_name,
56+
token,
57+
directory,
58+
retry_limit=10,
59+
description=None,
60+
):
61+
"""
62+
Create or update a GitHub release at https://github.com/<user>/<repo> for
63+
`tag_name` tag using the optional `description` for this release.
64+
Use the provided `token` as a GitHub token for API calls authentication.
65+
Upload all files found in the `directory` tree to that GitHub release.
66+
Retry API calls up to `retry_limit` time to work around instability the
67+
GitHub API.
68+
69+
Remote files that are not the same as the local files are deleted and re-
70+
uploaded.
71+
"""
72+
release_homepage_url = f'https://github.com/{user}/{repo}/releases/{tag_name}'
73+
74+
# scrape release page HTML for links
75+
urls_by_filename = {os.path.basename(l): l
76+
for l in utils_thirdparty.get_paths_or_urls(links_url=release_homepage_url)
77+
}
78+
79+
# compute what is new, modified or unchanged
80+
print(f'Compute which files is new, modified or unchanged in {release_homepage_url}')
81+
82+
new_to_upload = []
83+
unchanged_to_skip = []
84+
modified_to_delete_and_reupload = []
85+
for filename, pth, md5 in get_files(directory):
86+
url = urls_by_filename.get(filename)
87+
if not url:
88+
print(f'{filename} content is NEW, will upload')
89+
new_to_upload.append(pth)
90+
continue
91+
92+
out_of_date = get_etag_md5(url) != md5
93+
if out_of_date:
94+
print(f'{url} content is CHANGED based on md5 etag, will re-upload')
95+
modified_to_delete_and_reupload.append(pth)
96+
else:
97+
# print(f'{url} content is IDENTICAL, skipping upload based on Etag')
98+
unchanged_to_skip.append(pth)
99+
print('.')
100+
101+
ghapi = grr.GithubApi(
102+
github_api_url='https://api.github.com',
103+
user=user,
104+
repo=repo,
105+
token=token,
106+
retry_limit=retry_limit,
107+
)
108+
109+
# yank modified
110+
print(
111+
f'Unpublishing {len(modified_to_delete_and_reupload)} published but '
112+
f'locally modified files in {release_homepage_url}')
113+
114+
release = ghapi.get_release_by_tag(tag_name)
115+
116+
for pth in modified_to_delete_and_reupload:
117+
filename = os.path.basename(pth)
118+
asset_id = ghapi.find_asset_id_by_file_name(filename, release)
119+
print (f' Unpublishing file: {filename}).')
120+
response = ghapi.delete_asset(asset_id)
121+
if response.status_code != requests.codes.no_content: # NOQA
122+
raise Exception(f'failed asset deletion: {response}')
123+
124+
# finally upload new and modified
125+
to_upload = new_to_upload + modified_to_delete_and_reupload
126+
print(f'Publishing with {len(to_upload)} files to {release_homepage_url}')
127+
release = grr.Release(tag_name=tag_name, body=description)
128+
grr.make_release(ghapi, release, to_upload)
129+
130+
131+
TOKEN_HELP = (
132+
'The Github personal acess token is used to authenticate API calls. '
133+
'Required unless you set the GITHUB_TOKEN environment variable as an alternative. '
134+
'See for details: https://github.com/settings/tokens and '
135+
'https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token'
136+
)
137+
138+
139+
@click.command()
140+
141+
@click.option(
142+
'--user-repo-tag',
143+
help='The GitHub qualified repository user/name/tag in which '
144+
'to create the release such as in nexB/thirdparty/pypi',
145+
type=str,
146+
required=True,
147+
)
148+
@click.option(
149+
'-d', '--directory',
150+
help='The directory that contains files to upload to the release.',
151+
type=click.Path(exists=True, readable=True, path_type=str, file_okay=False, resolve_path=True),
152+
required=True,
153+
)
154+
@click.option(
155+
'--token',
156+
help=TOKEN_HELP,
157+
default=os.environ.get('GITHUB_TOKEN', None),
158+
type=str,
159+
required=False,
160+
)
161+
@click.option(
162+
'--description',
163+
help='Text description for the release. Ignored if the release exists.',
164+
default=None,
165+
type=str,
166+
required=False,
167+
)
168+
@click.option(
169+
'--retry_limit',
170+
help='Number of retries when making failing GitHub API calls. '
171+
'Retrying helps work around transient failures of the GitHub API.',
172+
type=int,
173+
default=10,
174+
)
175+
@click.help_option('-h', '--help')
176+
def publish_files(
177+
user_repo_tag,
178+
directory,
179+
retry_limit=10, token=None, description=None,
180+
):
181+
"""
182+
Publish all the files in DIRECTORY as assets to a GitHub release.
183+
Either create or update/replace remote files'
184+
"""
185+
if not token:
186+
click.secho('--token required option is missing.')
187+
click.secho(TOKEN_HELP)
188+
sys.exit(1)
189+
190+
user, repo, tag_name = user_repo_tag.split('/')
191+
192+
create_or_update_release_and_upload_directory(
193+
user=user,
194+
repo=repo,
195+
tag_name=tag_name,
196+
description=description,
197+
retry_limit=retry_limit,
198+
token=token,
199+
directory=directory,
200+
)
201+
202+
203+
if __name__ == '__main__':
204+
publish_files()

0 commit comments

Comments
 (0)