Skip to content

Commit

Permalink
Add composite action to create upstream tag (#5)
Browse files Browse the repository at this point in the history
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
ochosi authored Mar 21, 2022
1 parent 4e5debf commit ad2e704
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 56 deletions.
9 changes: 5 additions & 4 deletions README.md
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
62 changes: 25 additions & 37 deletions action.yml
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
9 changes: 0 additions & 9 deletions bump-version.sh

This file was deleted.

199 changes: 199 additions & 0 deletions create_tag.py
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()
6 changes: 0 additions & 6 deletions release-info.sh

This file was deleted.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ghapi

0 comments on commit ad2e704

Please sign in to comment.