-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add composite action to create upstream tag (#5)
Add composite action to create upstream tag The release_bot.py contains several steps, which were previously part of our release script in osbuild/maintainer-tools. The script here is much shorter though. Things that were dropped were the sanity checks, because we can presume that the git repository at hand is sane, that the git remote is well configured etc. Furthermore all interactivity of the original script was removed. Even though I had recently added a --no-interactive mode to the original script, I just felt this would make the code harder to read. The rationale for making this a composite action instead of a standalone repository was that this way each component can define it's own automated schedule and that there would be some representation in the GitHub Actions for the component of the tag being created. Getting the component/repository name as a separate step vs. just using `github.event.repository.name` is done so it also works when being triggered from schedule (the event payload is missing in this case).
- Loading branch information
Showing
6 changed files
with
230 additions
and
56 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 |
---|---|---|
@@ -1,8 +1,9 @@ | ||
## release-action | ||
|
||
This GitHub composite action is used for creating upstream releases in GitHub. | ||
This GitHub composite action is used for creating upstream git tags. | ||
It does the following: | ||
|
||
* Extracts the release note text from the git tag | ||
* Creates a GitHub release based on the tag information | ||
* Automatically bumps the version (where applicable) and commits this change | ||
* Bump the version number (based on the latest existing tag) | ||
* Extract the release note text from the pull requests associated with commits since the latest tag | ||
* Create a git tag with the release note text being the tag's body | ||
* Push the tag to the upstream repository |
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 |
---|---|---|
@@ -1,57 +1,45 @@ | ||
name: "Upstream release" | ||
description: "Perform an upstream GitHub release" | ||
name: "Release bot" | ||
description: "Create a tag and push it" | ||
|
||
inputs: | ||
token: | ||
description: "A GitHub token for creating a release" | ||
description: "A GitHub token for creating a tag" | ||
required: true | ||
slack_webhook_url: | ||
description: "A Slack incoming Webhook URL" | ||
username: | ||
description: "A GitHub user name" | ||
required: true | ||
email: | ||
description: "The GitHub user's email address" | ||
required: true | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- name: Checkout current repo | ||
uses: actions/checkout@v2 | ||
uses: actions/checkout@v3 | ||
with: | ||
token: "${{ inputs.TOKEN }}" | ||
fetch-depth: 0 | ||
|
||
- name: Fetch all tags | ||
# GitHub's checkout action doesn't properly fetch the current tag | ||
run: git fetch --tags --prune --force | ||
- name: Install Python and depends | ||
run: | | ||
sudo apt install python3 | ||
pip install -r ${{ github.action_path }}/requirements.txt | ||
shell: bash | ||
|
||
- name: Generate release information | ||
run: ${{ github.action_path }}/release-info.sh | ||
- name: Get component name | ||
run: echo "REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV | ||
shell: bash | ||
|
||
- name: Create GitHub release | ||
# https://github.com/marketplace/actions/create-release | ||
uses: ncipollo/release-action@v1.8.10 | ||
with: | ||
name: "${{ env.release_version }}" | ||
token: "${{ inputs.TOKEN }}" | ||
bodyFile: "release.md" | ||
|
||
- name: Bump version | ||
run: ${{ github.action_path }}/bump-version.sh "${{ env.release_version }}" | ||
- name: Set up git | ||
run: | | ||
git clone https://"${{ inputs.TOKEN }}"@github.com/"${{ github.repository }}".git ${{ github.action_path }}/${{ env.REPOSITORY_NAME }} | ||
git config --global user.email "${{ inputs.EMAIL }}" | ||
git config --global user.name "${{ inputs.USERNAME }}" | ||
shell: bash | ||
|
||
- name: Commit new version | ||
# https://github.com/marketplace/actions/add-commit | ||
uses: EndBug/add-and-commit@v7 | ||
with: | ||
branch: main | ||
message: "Post release version bump\n\n[skip ci]" | ||
add: "-u *" | ||
|
||
- name: Send release announcement to osbuild Slack channel | ||
id: slack | ||
uses: slackapi/slack-github-action@v1.16.0 | ||
with: | ||
payload: "{\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"🚀 *<https://github.com/osbuild/${{ env.component }}/releases/tag/v${{ env.release_version }}|${{ env.component }} ${{ env.release_version }}>* just got released upstream! 🚀\"}}]}" | ||
env: | ||
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }} | ||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK | ||
- name: Create and push tag | ||
run: | | ||
cd ${{ github.action_path }}/${{ env.REPOSITORY_NAME }}/ | ||
${{ github.action_path }}/create_tag.py --token "${{ inputs.TOKEN }}" | ||
shell: bash |
This file was deleted.
Oops, something went wrong.
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,199 @@ | ||
#!/usr/bin/python3 | ||
|
||
"""Release bot""" | ||
|
||
import argparse | ||
import subprocess | ||
import sys | ||
import os | ||
import time | ||
import logging | ||
from datetime import date | ||
from ghapi.all import GhApi | ||
|
||
|
||
class fg: # pylint: disable=too-few-public-methods | ||
"""Set of constants to print colored output in the terminal""" | ||
BOLD = '\033[1m' # bold | ||
OK = '\033[32m' # green | ||
INFO = '\033[33m' # yellow | ||
ERROR = '\033[31m' # red | ||
RESET = '\033[0m' # reset | ||
|
||
|
||
def msg_error(body): | ||
"""Print error and exit""" | ||
print(f"{fg.ERROR}{fg.BOLD}Error:{fg.RESET} {body}") | ||
sys.exit(1) | ||
|
||
|
||
def msg_info(body): | ||
"""Print info message""" | ||
print(f"{fg.INFO}{fg.BOLD}Info:{fg.RESET} {body}") | ||
|
||
|
||
def msg_ok(body): | ||
"""Print ok status message""" | ||
print(f"{fg.OK}{fg.BOLD}OK:{fg.RESET} {body}") | ||
|
||
|
||
def run_command(argv): | ||
"""Run a shellcommand and return stdout""" | ||
result = subprocess.run( # pylint: disable=subprocess-run-check | ||
argv, | ||
capture_output=True, | ||
text=True, | ||
encoding='utf-8') | ||
|
||
if result.returncode == 0: | ||
ret = result.stdout.strip() | ||
else: | ||
ret = result.stderr.strip() | ||
|
||
return ret | ||
|
||
|
||
def autoincrement_version(latest_tag): | ||
"""Bump the version of the latest git tag by 1""" | ||
if latest_tag == "": | ||
msg_info("There are no tags yet in this repository.") | ||
version = "1" | ||
elif "." in latest_tag: | ||
version = latest_tag.replace("v", "").split(".")[0] + "." + str(int(latest_tag[-1]) + 1) | ||
else: | ||
version = int(latest_tag.replace("v", "")) + 1 | ||
return version | ||
|
||
|
||
def list_prs_for_hash(args, api, repo, commit_hash): | ||
"""Get pull request for a given commit hash""" | ||
query = f'{commit_hash} type:pr is:merged base:{args.base} repo:osbuild/{repo}' | ||
try: | ||
res = api.search.issues_and_pull_requests(q=query, per_page=20) | ||
except: | ||
msg_info(f"Couldn't get PR infos for {commit_hash}.") | ||
res = None | ||
|
||
if res is not None: | ||
items = res["items"] | ||
|
||
if len(items) == 1: | ||
ret = items[0] | ||
else: | ||
msg_info(f"There are {len(items)} pull requests associated with {commit_hash} - skipping...") | ||
for item in items: | ||
msg_info(f"{item.html_url}") | ||
ret = None | ||
else: | ||
ret = None | ||
|
||
return ret | ||
|
||
|
||
def get_pullrequest_infos(args, repo, hashes): | ||
"""Fetch the titles of all related pull requests""" | ||
logging.debug("Collect pull request titles...") | ||
api = GhApi(repo=repo, owner='osbuild', token=args.token) | ||
|
||
for i, commit_hash in enumerate(hashes): | ||
print(f"Fetching PR for commit {i}/{len(hashes)} ({commit_hash})") | ||
time.sleep(2) | ||
pull_request = list_prs_for_hash(args, api, repo, commit_hash) | ||
if pull_request is not None: | ||
if repo == "cockpit-composer": | ||
msg = f"- {pull_request.title} (#{pull_request.number})" | ||
else: | ||
msg = f" * {pull_request.title} (#{pull_request.number})" | ||
summaries.append(msg) | ||
|
||
summaries = list(dict.fromkeys(summaries)) | ||
msg_ok(f"Collected summaries from {len(summaries)} pull requests ({i} commits).") | ||
return "\n".join(summaries) | ||
|
||
|
||
def get_contributors(latest_tag): | ||
"""Collect all contributors to a release based on the git history""" | ||
logging.debug("Collect contributors...") | ||
contributors = run_command(["git", "log", '--format="%an"', f"{latest_tag}..HEAD"]) | ||
contributor_list = contributors.replace('"', '').split("\n") | ||
names = "" | ||
for name in sorted(set(contributor_list)): | ||
if name != "": | ||
names += f"{name}, " | ||
|
||
logging.debug(f"List of contributors:\n{names[:-2]}") | ||
|
||
return names[:-2] | ||
|
||
|
||
def create_release_tag(args, repo, tag, latest_tag): | ||
"""Create a release tag""" | ||
logging.debug("Preparing tag...") | ||
today = date.today() | ||
contributors = get_contributors(latest_tag) | ||
|
||
summaries = "" | ||
hashes = run_command(['git', 'log', '--format=%H', f'{latest_tag}..HEAD']).split("\n") | ||
msg_info(f"Found {len(hashes)} commits since {latest_tag} in {args.base}:") | ||
logging.debug("\n".join(hashes)) | ||
summaries = get_pullrequest_infos(args, repo, hashes) | ||
|
||
tag = f'v{args.version}' | ||
message = (f"CHANGES WITH {args.version}:\n\n" | ||
f"----------------\n" | ||
f"{summaries}\n\n" | ||
f"Contributions from: {contributors}\n\n" | ||
f"— Somewhere on the Internet, {today.strftime('%Y-%m-%d')}") | ||
|
||
res = run_command(['git', 'tag', '-m', message, tag, 'HEAD']) | ||
logging.debug(res) | ||
msg_ok(f"Created tag '{tag}' with message:\n{message}") | ||
|
||
|
||
def print_config(args, repo): | ||
"""Print the values used for the release playbook""" | ||
print("\n--------------------------------\n" | ||
f"{fg.BOLD}Release:{fg.RESET}\n" | ||
f" Component: {repo}\n" | ||
f" Version: {args.version}\n" | ||
f" Base branch: {args.base}\n" | ||
f"--------------------------------\n") | ||
|
||
|
||
def main(): | ||
"""Main function""" | ||
# Get some basic fallback/default values | ||
repo = os.path.basename(os.getcwd()) | ||
latest_tag = run_command(['git', 'describe', '--tags', '--abbrev=0']) | ||
version = autoincrement_version(latest_tag) | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument("-v", "--version", | ||
help=f"Set the version for the release (Default: {version})", | ||
default=version) | ||
parser.add_argument("-t", "--token", help=f"Set the GitHub token") | ||
parser.add_argument("-b", "--base", | ||
help=f"Set the release branch (Default: 'main')", | ||
default='main') | ||
parser.add_argument("-d", "--debug", help="Print lots of debugging statements", action="store_const", | ||
dest="loglevel", const=logging.DEBUG, default=logging.INFO) | ||
args = parser.parse_args() | ||
|
||
logging.basicConfig(level=args.loglevel, format='%(asctime)s %(message)s', datefmt='%Y/%m/%d/ %H:%M:%S') | ||
|
||
print_config(args, repo) | ||
|
||
tag = f'v{args.version}' | ||
logging.debug(f"Current release: {latest_tag}\nNew release: {args.version}\nTag name: {tag}") | ||
|
||
# Create a release tag | ||
create_release_tag(args, repo, tag, latest_tag) | ||
|
||
# Push the tag | ||
res = run_command(['git', 'push', 'origin', tag]) | ||
logging.debug(res) | ||
msg_ok(f"Pushed tag '{tag}' to branch '{args.base}'") | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file was deleted.
Oops, something went wrong.
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 @@ | ||
ghapi |