From 50dd136e9a36b597544113df8e3d7331189b457d Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Wed, 10 Aug 2022 12:48:58 +0100 Subject: [PATCH] Automated release process Signed-off-by: Pedro Algarvio --- .../{checksums.yml => checksums.yml.bak} | 4 +- .github/workflows/release.yml | 217 ++++++++++++++++-- .github/workflows/scripts/cut-release.py | 203 ++++++++++++++++ 3 files changed, 396 insertions(+), 28 deletions(-) rename .github/workflows/{checksums.yml => checksums.yml.bak} (84%) create mode 100644 .github/workflows/scripts/cut-release.py diff --git a/.github/workflows/checksums.yml b/.github/workflows/checksums.yml.bak similarity index 84% rename from .github/workflows/checksums.yml rename to .github/workflows/checksums.yml.bak index 091e3a6..b03fde4 100644 --- a/.github/workflows/checksums.yml +++ b/.github/workflows/checksums.yml.bak @@ -9,22 +9,20 @@ jobs: checksums: name: Update Scripts Checksums runs-on: ubuntu-latest + if: github.repository == 'saltstack/salt-bootstrap' steps: - uses: actions/checkout@v2 - if: github.repository == 'saltstack/salt-bootstrap' with: ref: stable - name: Get bootstrap-salt.sh sha256sum - if: github.repository == 'saltstack/salt-bootstrap' run: | echo "SH=$(sha256sum bootstrap-salt.sh | awk '{ print $1 }')" >> $GITHUB_ENV echo "PS1=$(sha256sum bootstrap-salt.ps1 | awk '{ print $1 }')" >> $GITHUB_ENV echo "BS_VERSION=$(sh bootstrap-salt.sh -v | awk '{ print $4 }')" >> $GITHUB_ENV - name: Update Checksums - if: github.repository == 'saltstack/salt-bootstrap' run: | echo ${{ env.SH }} > bootstrap-salt.sh.sha256 echo ${{ env.PS1 }} > bootstrap-salt.ps1.sha256 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c00c5cd..a42557b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,45 +1,210 @@ -name: Release +name: Cut Release -on: - push: - tags: - - '*' +on: workflow_dispatch jobs: - bootstrap: - name: Update Release Checksums on Develop + update-develop: + name: Update CHANGELOG.md and bootstrap-salt.sh runs-on: ubuntu-latest - + if: github.repository == 'saltstack/salt-bootstrap' + permissions: + contents: write # To be able to publish the release steps: - - uses: actions/checkout@v2 - if: github.repository == 'saltstack/salt-bootstrap' + - name: Check Branch Triggering Release + run: | + if [ "${{ github.ref_name }}" != "develop" ] + then + echo "This workflow should only be triggered from the develop branch" + exit 1 + fi + - uses: actions/checkout@v3 + with: + ref: develop + repository: ${{ github.repository }} + + - name: Update Git Settings + run: | + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot] on behalf of ${{ github.event.sender.login }}" + + - name: Set up Python 3.7 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + - name: Install Requirements + run: | + python3 -m pip install requests pre-commit + pre-commit install --install-hooks + + - name: Update Repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 .github/workflows/scripts/cut-release.py --repo ${{ github.repository }} + export CUT_RELEASE_VERSION=$(cat .cut_release_version) + export CUT_RELEASE_CHANGES=$(cat .cut_release_changes) + echo "CUT_RELEASE_VERSION=${CUT_RELEASE_VERSION}" >> $GITHUB_ENV + echo "CUT_RELEASE_CHANGES=${CUT_RELEASE_CHANGES}" >> $GITHUB_ENV + + - name: Show Changes + run: | + git status + git diff + + - name: Commit Changes + run: | + git commit -am "Update develop branch for the ${CUT_RELEASE_VERSION} release" || \ + git commit -am "Update develop branch for the ${CUT_RELEASE_VERSION} release" + + - name: Push Changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: develop + + - name: Upload Release Details + uses: actions/upload-artifact@v3 + with: + name: release-details + path: | + .cut_release_version + .cut_release_changes + + merge-develop-into-stable: + name: Merge develop into stable + runs-on: ubuntu-latest + if: github.repository == 'saltstack/salt-bootstrap' + needs: update-develop + permissions: + contents: write # To be able to publish the release + steps: + - uses: actions/checkout@v3 with: ref: stable + repository: ${{ github.repository }} + fetch-depth: 0 + + - name: Update Git Settings + run: | + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot] on behalf of @${{ github.event.sender.login }}" + + - name: Download Release Details + uses: actions/download-artifact@v3 + with: + name: release-details + + - name: Update Environment + run: | + export CUT_RELEASE_VERSION=$(cat .cut_release_version) + export CUT_RELEASE_CHANGES=$(cat .cut_release_changes) + echo "CUT_RELEASE_VERSION=${CUT_RELEASE_VERSION}" >> $GITHUB_ENV + echo "CUT_RELEASE_CHANGES=${CUT_RELEASE_CHANGES}" >> $GITHUB_ENV + + - name: Merge develop into stable + run: | + git merge --no-ff -m "Merge develop into stable" origin/develop || touch .git-conflicts + if [ -f .git-conflicts ] + then + git diff + for f in $(git status | grep 'both modified' | awk '{ print $3 }') + do + git checkout --theirs $f + git add $f + done + git commit -a -m "Merge develop into stable(auto resolving conflicts to the develop version)" + fi + + - name: Tag Release + uses: mathieudutour/github-tag-action@v6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ env.CUT_RELEASE_VERSION }} + tag_prefix: "" + create_annotated_tag: true + + - name: Push Changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: stable + tags: true + + publish-release: + name: Create GitHub Release + runs-on: ubuntu-latest + if: github.repository == 'saltstack/salt-bootstrap' + needs: merge-develop-into-stable + permissions: + contents: write # To be able to publish the release + steps: + - uses: actions/checkout@v3 + with: + ref: stable + repository: ${{ github.repository }} + - name: Download Release Details + uses: actions/download-artifact@v3 + with: + name: release-details + + - name: Update Environment + run: | + export CUT_RELEASE_VERSION=$(cat .cut_release_version) + export CUT_RELEASE_CHANGES=$(cat .cut_release_changes) + echo "CUT_RELEASE_VERSION=${CUT_RELEASE_VERSION}" >> $GITHUB_ENV + echo "CUT_RELEASE_CHANGES=${CUT_RELEASE_CHANGES}" >> $GITHUB_ENV + + - name: Create Github Release + uses: softprops/action-gh-release@v1 + with: + name: ${{ env.CUT_RELEASE_VERSION }} + tag_name: ${{ env.CUT_RELEASE_VERSION }} + body_path: .cut_release_changes + target_commitish: stable + prerelease: false + generate_release_notes: false + files: | + bootstrap-salt.sh + bootstrap-salt.ps1 + LICENSE + + update-develop-checksums: + name: Update Release Checksums on Develop + runs-on: ubuntu-latest + if: github.repository == 'saltstack/salt-bootstrap' + needs: publish-release + permissions: + contents: write # For action peter-evans/create-pull-request + pull-requests: write # For action peter-evans/create-pull-request + + steps: + - uses: actions/checkout@v3 + with: + ref: stable + repository: ${{ github.repository }} - name: Get bootstrap-salt.sh sha256sum - if: github.repository == 'saltstack/salt-bootstrap' run: | echo "SH=$(sha256sum bootstrap-salt.sh | awk '{ print $1 }')" >> $GITHUB_ENV echo "BS_VERSION=$(sh bootstrap-salt.sh -v | awk '{ print $4 }')" >> $GITHUB_ENV - - uses: actions/checkout@v2 - if: github.repository == 'saltstack/salt-bootstrap' + - uses: actions/checkout@v3 with: ref: develop + repository: ${{ github.repository }} - name: Set up Python 3.7 - if: github.repository == 'saltstack/salt-bootstrap' - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Update Latest Release on README - if: github.repository == 'saltstack/salt-bootstrap' run: | python3 .github/workflows/scripts/update-release-shasum.py ${{ env.BS_VERSION }} ${{ env.SH }} - name: Create Pull Request Against Develop - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4 with: title: Update README.rst with ${{ env.BS_VERSION }} release sha256sum commit-message: Update README.rst with ${{ env.BS_VERSION }} release sha256sum @@ -48,34 +213,36 @@ jobs: salt: name: Update Release on Salt Repo runs-on: ubuntu-latest + if: github.repository == 'saltstack/salt-bootstrap' + needs: update-develop-checksums + permissions: + contents: write # For action peter-evans/create-pull-request + pull-requests: write # For action peter-evans/create-pull-request steps: - - uses: actions/checkout@v2 - if: github.repository == 'saltstack/salt-bootstrap' + - uses: actions/checkout@v3 with: ref: stable + repository: ${{ github.repository }} - name: Get bootstrap version - if: github.repository == 'saltstack/salt-bootstrap' run: | echo "BS_VERSION=$(sh bootstrap-salt.sh -v | awk '{ print $4 }')" >> $GITHUB_ENV - - uses: actions/checkout@v2 - if: github.repository == 'saltstack/salt-bootstrap' + - uses: actions/checkout@v3 with: repository: saltstack/salt ref: master path: salt-checkout - name: Update bootstrap script on Salt - if: github.repository == 'saltstack/salt-bootstrap' run: | cp bootstrap-salt.sh salt-checkout/salt/cloud/deploy/bootstrap-salt.sh - name: Create Pull Request Against Develop - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4 with: - title: Update the bootstrap script to v${{ env.BS_VERSION }} + title: "[DO NOT MERGE] Update the bootstrap script to v${{ env.BS_VERSION }}" path: salt-checkout commit-message: Update the bootstrap script to v${{ env.BS_VERSION }} delete-branch: true diff --git a/.github/workflows/scripts/cut-release.py b/.github/workflows/scripts/cut-release.py new file mode 100644 index 0000000..4cbda55 --- /dev/null +++ b/.github/workflows/scripts/cut-release.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +import os +import re +import sys +import pathlib +import argparse +import requests +from datetime import datetime + +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent.parent.parent + + +class ClassPropertyDescriptor: + def __init__(self, fget, fset=None): + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + if klass is None: + klass = type(obj) + return self.fget.__get__(obj, klass)() + + def __set__(self, obj, value): + if not self.fset: + raise AttributeError("can't set attribute") + type_ = type(obj) + return self.fset.__get__(obj, type_)(value) + + def setter(self, func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + self.fset = func + return self + + +def classproperty(func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + + return ClassPropertyDescriptor(func) + + +class Session: + + _instance = None + + def __init__(self, endpoint=None): + if endpoint is None: + endpoint = "https://api.github.com" + self.endpoint = endpoint + self.session = requests.Session() + self.session.headers.update( + { + "Accept": "application/vnd.github+json", + "Authorization": f"token {os.environ['GITHUB_TOKEN']}", + } + ) + + @classproperty + def instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def get(self, path, **kwargs): + return self.session.get(f"{self.endpoint}/{path.lstrip('/')}", **kwargs) + + def post(self, path, **kwargs): + return self.session.post(f"{self.endpoint}/{path.lstrip('/')}", **kwargs) + + def __enter__(self): + self.session.__enter__() + return self + + def __exit__(self, *args): + self.session.__exit__(*args) + + +def get_latest_release(options): + response = Session.instance.get(f"/repos/{options.repo}/releases/latest") + if response.status_code != 404: + return response.json()["tag_name"] + + print( + f"Failed to get latest release. HTTP Response:\n{response.text}", + file=sys.stderr, + flush=True, + ) + print("Searching tags...", file=sys.stderr, flush=True) + + tags = [] + page = 0 + while True: + page += 1 + response = Session.instance.get( + f"/repos/{options.repo}/tags", data={"pre_page": 100, "page": page} + ) + repo_tags = response.json() + added_tags = False + for tag in repo_tags: + if tag["name"] not in tags: + tags.append(tag["name"]) + added_tags = True + if added_tags is False: + break + + return list(sorted(tags))[-1] + + +def get_generated_changelog(options): + response = Session.instance.post( + f"/repos/{options.repo}/releases/generate-notes", + json={ + "tag_name": options.release_tag, + "previous_tag_name": options.previous_tag, + "target_commitish": "develop", + }, + ) + if response.status_code == 200: + return response.json() + return response.text + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True, help="The / to use") + parser.add_argument("--release-tag", required=False, help="The tag of the release") + parser.add_argument( + "--previous-tag", + required=False, + help="The previous release tag. If not passed, the GH Api will be queried for it.", + ) + + changelog_file = REPO_ROOT / "CHANGELOG.md" + + if not os.environ.get("GITHUB_TOKEN"): + parser.exit(status=1, message="GITHUB_TOKEN environment variable not set") + + options = parser.parse_args() + if not options.release_tag: + options.release_tag = f"v{datetime.utcnow().strftime('%Y.%m.%d')}" + if not options.previous_tag: + options.previous_tag = get_latest_release(options) + + print( + f"Creating changelog entries from {options.previous_tag} to {options.release_tag} ...", + file=sys.stderr, + flush=True, + ) + + changelog = get_generated_changelog(options) + if not isinstance(changelog, dict): + parser.exit( + status=1, + message=f"Unable to generate changelog. HTTP Response:\n{changelog}", + ) + + cut_release_version = REPO_ROOT / ".cut_release_version" + print( + f"* Writing {cut_release_version.relative_to(REPO_ROOT)} ...", + file=sys.stderr, + flush=True, + ) + cut_release_version.write_text(options.release_tag) + + cut_release_changes = REPO_ROOT / ".cut_release_changes" + print( + f"* Writing {cut_release_changes.relative_to(REPO_ROOT)} ...", + file=sys.stderr, + flush=True, + ) + cut_release_changes.write_text(changelog["body"]) + + print( + f"* Updating {changelog_file.relative_to(REPO_ROOT)} ...", + file=sys.stderr, + flush=True, + ) + changelog_file.write_text( + f"# {changelog['name']}\n\n" + + changelog["body"] + + "\n\n" + + changelog_file.read_text() + ) + + bootstrap_script_path = REPO_ROOT / "bootstrap-salt.sh" + print( + f"* Updating {bootstrap_script_path.relative_to(REPO_ROOT)} ...", + file=sys.stderr, + flush=True, + ) + bootstrap_script_path.write_text( + re.sub( + r'__ScriptVersion="(.*)"', + f'__ScriptVersion="{options.release_tag.lstrip("v")}"', + bootstrap_script_path.read_text(), + ) + ) + parser.exit(status=0, message="CHANGELOG.md and bootstrap-salt.sh updated\n") + + +if __name__ == "__main__": + main()