-
Notifications
You must be signed in to change notification settings - Fork 268
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New release script automating announcements postings
Handles the following channels: - GitHub (Release creation, closing Milestone) - Gitter - Twitter
- Loading branch information
Showing
4 changed files
with
405 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
""" | ||
ADOdb release management scripts utilities and helper classes. | ||
- Environment class | ||
Reads configuration variables from the environment file, and makes them | ||
available in the 'env' global variable.. | ||
- Gitter class | ||
Use Gitter REST API to post announcements | ||
This file is part of ADOdb, a Database Abstraction Layer library for PHP. | ||
@package ADOdb | ||
@link https://adodb.org Project's web site and documentation | ||
@link https://github.com/ADOdb/ADOdb Source code and issue tracker | ||
The ADOdb Library is dual-licensed, released under both the BSD 3-Clause | ||
and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option, | ||
any later version. This means you can use it in proprietary products. | ||
See the LICENSE.md file distributed with this source code for details. | ||
@license BSD-3-Clause | ||
@license LGPL-2.1-or-later | ||
@copyright 2022 Damien Regad, Mark Newnham and the ADOdb community | ||
@author Damien Regad | ||
""" | ||
|
||
from os import path | ||
|
||
import requests | ||
import yaml | ||
|
||
|
||
class Environment: | ||
# See env.yml.sample for details about these config variables | ||
sf_api_key = None | ||
|
||
github_token = None | ||
github_repo = 'ADOdb/ADOdb' | ||
|
||
gitter_token = None | ||
gitter_room = 'ADOdb/ADOdb' | ||
|
||
twitter_account = 'ADOdb_announce' | ||
twitter_api_key = None | ||
twitter_api_secret = None | ||
twitter_bearer_token = None # Currently unused | ||
twitter_access_token = None | ||
twitter_access_secret = None | ||
|
||
def __init__(self, filename='env.yml'): | ||
""" | ||
Constructor - load the config file and initialize properties. | ||
:param filename: Name of YAML config file to load | ||
""" | ||
env_file = path.join(path.dirname(path.abspath(__file__)), filename) | ||
|
||
# Read the config file | ||
try: | ||
with open(env_file, 'r') as stream: | ||
config = yaml.safe_load(stream) | ||
except yaml.parser.ParserError as e: | ||
raise Exception("Invalid Environment file") from e | ||
|
||
# Assign class properties from config | ||
for key, value in config.items(): | ||
setattr(self, key, value) | ||
|
||
|
||
class Gitter: | ||
base_url = 'https://api.gitter.im/v1/' | ||
|
||
_headers = '' | ||
_room_id = '' | ||
|
||
def __init__(self, token, room_name): | ||
""" | ||
Class Constructor. | ||
:param token: Gitter REST API token (see https://developer.gitter.im/apps) | ||
:param room_name: Room name, e.g. `ADOdb/ADOdb` | ||
""" | ||
self._headers = { | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json', | ||
'Authorization': 'Bearer ' + token.strip() | ||
} | ||
|
||
# Initialize Room Id | ||
if not room_name: | ||
raise Exception("Gitter Room Name not defined") | ||
r = requests.get(self.url('rooms'), | ||
headers=self._headers, | ||
params={'q': room_name}) | ||
if r.status_code != requests.codes.ok: | ||
raise Exception(r.text) | ||
|
||
for room in r.json()['results']: | ||
if room['name'] == room_name: | ||
self._room_id = room['id'] | ||
if not self._room_id: | ||
raise Exception("Gitter Room '{}' not found".format(room_name)) | ||
|
||
def url(self, endpoint): | ||
""" | ||
Get Gitter REST API URL for the given endpoint. | ||
:param endpoint: REST API endpoint | ||
:return: URL | ||
""" | ||
return self.base_url + endpoint | ||
|
||
def post(self, message): | ||
""" | ||
Post a message to a Gitter room. | ||
:param message: Message text | ||
:return: Posted message's Id | ||
""" | ||
url = self.url('rooms/{}/chatMessages'.format(self._room_id)) | ||
r = requests.post(url, | ||
headers=self._headers, | ||
json={'text': message}) | ||
if r.status_code != requests.codes.ok: | ||
raise Exception(r.text) | ||
return r.json()['id'] | ||
|
||
|
||
# Initialize environment | ||
env = Environment() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
#!/usr/bin/env -S python3 -u | ||
""" | ||
ADOdb announcements script. | ||
Posts release announcements to | ||
- Gitter | ||
This file is part of ADOdb, a Database Abstraction Layer library for PHP. | ||
@package ADOdb | ||
@link https://adodb.org Project's web site and documentation | ||
@link https://github.com/ADOdb/ADOdb Source code and issue tracker | ||
The ADOdb Library is dual-licensed, released under both the BSD 3-Clause | ||
and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option, | ||
any later version. This means you can use it in proprietary products. | ||
See the LICENSE.md file distributed with this source code for details. | ||
@license BSD-3-Clause | ||
@license LGPL-2.1-or-later | ||
@copyright 2022 Damien Regad, Mark Newnham and the ADOdb community | ||
@author Damien Regad | ||
""" | ||
|
||
import argparse | ||
from datetime import date | ||
import json | ||
import re | ||
from pathlib import Path | ||
|
||
import tweepy # https://www.tweepy.org/ | ||
from git import Repo # https://gitpython.readthedocs.io | ||
# https://github.com/PyGithub/PyGithub | ||
from github import Github, GithubException, Milestone | ||
|
||
from adodbutil import env, Gitter | ||
|
||
|
||
def process_command_line(): | ||
""" | ||
Parse command-line options | ||
:return: Namespace | ||
""" | ||
# Get most recent Git tag | ||
repo = Repo(path=Path(__file__).parents[1]) | ||
tags = sorted(repo.tags, key=lambda t: t.tag.tagged_date) | ||
latest_tag = str(tags[-1]) | ||
|
||
parser = argparse.ArgumentParser( | ||
description="Post ADOdb release announcement messages to Gitter." | ||
) | ||
parser.add_argument('version', | ||
nargs='?', | ||
default=latest_tag, | ||
help="Version number to announce; if not specified, " | ||
"the latest tag will be used.") | ||
parser.add_argument('-m', '--message', | ||
help="Additional text to add to announcement message") | ||
parser.add_argument('-b', '--batch', | ||
action="store_true", | ||
help="Batch mode - do not ask for confirmation " | ||
"before posting") | ||
|
||
only = parser.add_mutually_exclusive_group() | ||
only.add_argument('-g', '--gitter-only', | ||
action="store_true", | ||
help="Only post the announcement to Gitter") | ||
only.add_argument('-t', '--twitter-only', | ||
action="store_true", | ||
help="Only post the announcement to Twitter") | ||
only.add_argument('-u', '--github-only', | ||
action="store_true", | ||
help="Only post the announcement to GitHub") | ||
|
||
return parser.parse_args() | ||
|
||
|
||
def github_close_milestone(repo, version): | ||
print(f"Closing Milestone '{version}'") | ||
|
||
# Search Milestone for version | ||
milestone_found = False | ||
milestone: Milestone.Milestone | ||
for milestone in repo.get_milestones(): | ||
if milestone.title == version: | ||
milestone_found = True | ||
break | ||
|
||
# Milestone not found, check if already closed | ||
if not milestone_found: | ||
# Process closed Milestones in reverse order of due_on, to minimize | ||
# number of iterations | ||
for milestone in repo.get_milestones(state='closed', | ||
sort='due_on', | ||
direction='desc'): | ||
if milestone.title == version: | ||
print(f"Already closed {milestone.raw_data['html_url']}") | ||
return | ||
raise Exception(f"Milestone '{version}' not found") | ||
|
||
# Close the milestone | ||
# noinspection PyUnboundLocalVariable | ||
milestone.edit(title=milestone.title, | ||
state='closed', | ||
due_on=date.today()) | ||
|
||
|
||
def post_github(version, message, changelog_link): | ||
print(f"GitHub Release for repository '{env.github_repo}'") | ||
|
||
gh = Github(env.github_token) | ||
repo = gh.get_repo(env.github_repo) | ||
|
||
# Check if Release already exists | ||
version = 'v' + version | ||
try: | ||
rel = repo.get_release(version) | ||
print(f"Existing release '{version}' found", rel.html_url) | ||
|
||
# Discard the message provided on command-line, and use the one from | ||
# the Release's description, inform user to update it on GitHub. | ||
if message: | ||
print(f"Your message will be discarded; " | ||
"the Release's description will be used instead.\n" | ||
"Edit it on GitHub if needed") | ||
else: | ||
print("Retrieving the Release's description for the " | ||
"announcement message") | ||
|
||
# Remove the changelog link to keep only the release's message | ||
message = re.sub(r"[,.]?\s*(Please )?See .*$", | ||
"", | ||
rel.body, | ||
flags=re.IGNORECASE).strip() | ||
if message: | ||
message += ".\n" | ||
except GithubException as err: | ||
if err.status != 404: | ||
raise err | ||
print(f"Release '{version}' does not exist yet") | ||
|
||
# Make sure the version has been tagged | ||
try: | ||
repo.get_git_ref('tags/' + version) | ||
print(f"Tag '{version}' found") | ||
except GithubException: | ||
print(f"ERROR: Tag '{version}' not found") | ||
exit(1) | ||
|
||
# Create the release | ||
rel = repo.create_git_release(version, | ||
version, | ||
message + changelog_link) | ||
print("Release created successfully", rel.html_url) | ||
|
||
print() | ||
|
||
# Closing the Milestone | ||
try: | ||
github_close_milestone(repo, version) | ||
except Exception as e: | ||
print(str(e)) | ||
exit(1) | ||
|
||
# Return message to be used for remaining announcements | ||
return message | ||
|
||
|
||
def post_gitter(message): | ||
print("Posting to Gitter... ", end='') | ||
gitter = Gitter(env.gitter_token, env.gitter_room) | ||
message_id = gitter.post('# ' + message) | ||
print("Message posted successfully\n" | ||
"https://gitter.im/{}?at={}" | ||
.format(env.gitter_room, message_id)) | ||
print() | ||
|
||
|
||
def post_twitter(message): | ||
print("Posting to Twitter... ", end='') | ||
twitter = tweepy.Client( | ||
consumer_key=env.twitter_api_key, | ||
consumer_secret=env.twitter_api_secret, | ||
access_token=env.twitter_access_token, | ||
access_token_secret=env.twitter_access_secret | ||
) | ||
try: | ||
r = twitter.create_tweet(text=message) | ||
except tweepy.errors.HTTPException as e: | ||
err = json.loads(e.response.text) | ||
print("ERROR") | ||
print(e, "-", err['detail']) | ||
return | ||
print("Tweeted successfully\n" | ||
"https://twitter.com/{}/status/{}" | ||
.format(env.twitter_account, r.data['id'])) | ||
print() | ||
|
||
|
||
def main(): | ||
args = process_command_line() | ||
|
||
post_everywhere = not args.gitter_only \ | ||
and not args.github_only \ | ||
and not args.twitter_only | ||
version = args.version.lstrip('v') | ||
changelog_url = f"https://github.com/ADOdb/ADOdb/blob/v{version}" \ | ||
"/docs/changelog.md" | ||
message = args.message.rstrip(".") + ".\n" if args.message else "" | ||
|
||
# Create GitHub release, retrieve message from it if it already exists | ||
if post_everywhere or args.github_only: | ||
message = post_github(version, | ||
message, | ||
f"See [Changelog]({changelog_url}) for details") | ||
if args.github_only: | ||
return | ||
|
||
# Build announcement message | ||
msg_announce = "ADOdb Version {0} released\n{1}{2}".format( | ||
version, | ||
message, | ||
"See Changelog " + changelog_url | ||
) | ||
|
||
# Get confirmation | ||
if not args.batch: | ||
print("Review ", end='') | ||
print("Announcement message") | ||
print("-" * 27) | ||
print(msg_announce) | ||
print("-" * 27) | ||
if not args.batch: | ||
reply = input("Proceed with posting ? ") | ||
if not reply.casefold() == 'y': | ||
print("Aborting") | ||
exit(1) | ||
print() | ||
|
||
if post_everywhere or args.gitter_only: | ||
post_gitter(msg_announce) | ||
if post_everywhere or args.twitter_only: | ||
post_twitter(msg_announce) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.