diff --git a/.github/workflows/release_charm.yaml b/.github/workflows/release_charm.yaml index 5f5f4021..a3caaf7d 100644 --- a/.github/workflows/release_charm.yaml +++ b/.github/workflows/release_charm.yaml @@ -22,7 +22,7 @@ on: default: . type: string create-github-release: - description: Create git tag & GitHub release + description: Create git tag(s) & GitHub release default: true type: boolean charmcraft-snap-revision: @@ -71,18 +71,14 @@ jobs: merge-multiple: true - name: Upload & release charm id: release - run: release-charm --directory='${{ inputs.path-to-charm-directory }}' --channel='${{ inputs.channel }}' --create-release-tags='${{ inputs.create-github-release }}' + run: release-charm --directory='${{ inputs.path-to-charm-directory }}' --channel='${{ inputs.channel }}' --create-release='${{ inputs.create-github-release }}' env: CHARMCRAFT_AUTH: ${{ secrets.charmhub-token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload charmcraft logs if: ${{ failure() && steps.release.outcome == 'failure' }} uses: actions/upload-artifact@v4 with: name: logs-charmcraft-release-${{ inputs.artifact-prefix }} path: ~/.local/state/charmcraft/log/ - if-no-files-found: error - - name: Create GitHub release - if: ${{ inputs.create-github-release }} - run: gh release create '${{ steps.release.outputs.release_tag }}' --verify-tag --generate-notes --title '${{ steps.release.outputs.release_title }}' --notes-file release_notes.txt - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if-no-files-found: ignore diff --git a/.github/workflows/release_rock.yaml b/.github/workflows/release_rock.yaml index 688465f3..676ede19 100644 --- a/.github/workflows/release_rock.yaml +++ b/.github/workflows/release_rock.yaml @@ -56,9 +56,6 @@ jobs: merge-multiple: true - name: Upload & release rock id: release - run: release-rock --directory='${{ inputs.path-to-rock-directory }}' --create-release-tag='${{ inputs.create-github-release }}' - - name: Create GitHub release - if: ${{ inputs.create-github-release }} - run: gh release create '${{ steps.release.outputs.release_tag }}' --verify-tag --generate-notes --title '${{ steps.release.outputs.release_title }}' --notes-file release_notes.txt + run: release-rock --directory='${{ inputs.path-to-rock-directory }}' --create-release='${{ inputs.create-github-release }}' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release_snap.yaml b/.github/workflows/release_snap.yaml index 236032fb..292dcef9 100644 --- a/.github/workflows/release_snap.yaml +++ b/.github/workflows/release_snap.yaml @@ -25,7 +25,7 @@ on: default: . type: string create-github-release: - description: Create git tag & GitHub release + description: Create git tag(s) & GitHub release default: true type: boolean snapcraft-snap-revision: @@ -74,19 +74,15 @@ jobs: merge-multiple: true - name: Upload & release snap id: release - run: release-snap --directory='${{ inputs.path-to-snap-project-directory }}' --channel='${{ inputs.channel }}' --create-release-tags='${{ inputs.create-github-release }}' + run: release-snap --directory='${{ inputs.path-to-snap-project-directory }}' --channel='${{ inputs.channel }}' --create-release='${{ inputs.create-github-release }}' env: SNAPCRAFT_STORE_AUTH: candid SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snap-store-token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload snapcraft logs if: ${{ failure() && steps.release.outcome == 'failure' }} uses: actions/upload-artifact@v4 with: name: logs-snapcraft-release-${{ inputs.artifact-prefix }} path: ~/.local/state/snapcraft/log/ - if-no-files-found: error - - name: Create GitHub release - if: ${{ inputs.create-github-release }} - run: gh release create '${{ steps.release.outputs.release_tag }}' --verify-tag --generate-notes --title '${{ steps.release.outputs.release_title }}' --notes-file release_notes.txt - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if-no-files-found: ignore diff --git a/python/cli/data_platform_workflows_cli/craft_tools/release.py b/python/cli/data_platform_workflows_cli/craft_tools/release.py index 7156fb24..80f6edb5 100644 --- a/python/cli/data_platform_workflows_cli/craft_tools/release.py +++ b/python/cli/data_platform_workflows_cli/craft_tools/release.py @@ -22,7 +22,7 @@ class OCIResource: revision: int -def run(command_: list): +def run(command_: list, *, log=True): """Run subprocess command & log stderr Returns: @@ -32,16 +32,60 @@ def run(command_: list): try: process.check_returncode() except subprocess.CalledProcessError as e: - logging.error(e.stderr) + if log: + logging.error(e.stderr) raise - return process.stdout + return process.stdout.strip() + + +def create_tags_and_release( + *, tags: list[str], release_tag: str, release_title: str, release_notes: str +): + """Create git tags and GitHub release""" + # Create git tags + logging.info("Pushing git tag(s)") + for tag in tags: + subprocess.run(["git", "tag", tag], check=True) + subprocess.run(["git", "push", "origin", tag], check=True) + # Create GitHub release + logging.info("Creating GitHub release") + # Manually retrieve the last release tag since GitHub's automatic selection of previous tag + # does not always work (specifically if there are older tags that do not have a release). + try: + last_release_tag = json.loads( + run(["gh", "release", "view", "--json", "tagName"], log=False) + )["tagName"] + except subprocess.CalledProcessError as e: + if e.stderr.strip() == "release not found": + last_release_tag = None + else: + logging.error(e.stderr) + raise + if not release_notes.endswith("\n"): + release_notes += "\n" + command = [ + "gh", + "release", + "create", + release_tag, + "--verify-tag", + "--generate-notes", + "--title", + release_title, + "--notes", + release_notes, + ] + if last_release_tag: + command.extend(("--notes-start-tag", last_release_tag)) + logging.info(f"Running {command=}") + subprocess.run(command, check=True) def snap(): parser = argparse.ArgumentParser() parser.add_argument("--directory", required=True) parser.add_argument("--channel", required=True) - parser.add_argument("--create-release-tags", required=True) + parser.add_argument("--create-release", required=True) args = parser.parse_args() directory = pathlib.Path(args.directory) @@ -57,41 +101,35 @@ class Revision: architecture = snap_file.name.removesuffix(".snap").split("_")[-1] logging.info(f"Uploading {snap_file=}") output = run(["snapcraft", "upload", "--release", args.channel, snap_file]) - # Example `output`: "Revision 3 created for 'charmed-postgresql' and released to 'latest/edge'\n" + # Example `output`: "Revision 3 created for 'charmed-postgresql' and released to 'latest/edge'" match = re.match("Revision ([0-9]+) created for ", output) assert match, "Unable to parse revision" revision = int(match.group(1)) logging.info(f"Uploaded snap {revision=} {architecture=}") revisions.append(Revision(value=revision, architecture=architecture)) - if json.loads(args.create_release_tags) is not True: + if json.loads(args.create_release) is not True: return - logging.info("Pushing git tag(s)") - # Create git tags - for revision in revisions: - tag = f"rev{revision.value}" - subprocess.run(["git", "tag", tag], check=True) - subprocess.run(["git", "push", "origin", tag], check=True) - # Output GitHub release info - release_tag = f"rev{max(revision.value for revision in revisions)}" - github_actions.output["release_tag"] = release_tag if len(revisions) == 1: release_title = "Revision " else: release_title = "Revisions " release_title += ", ".join(str(revision.value) for revision in revisions) - github_actions.output["release_title"] = release_title release_notes = f"Released to {args.channel}" for revision in revisions: release_notes += f"\n- {revision.architecture}: revision {revision.value}" - with open("release_notes.txt", "w") as file: - file.write(release_notes) + create_tags_and_release( + tags=[f"rev{revision.value}" for revision in revisions], + release_tag=f"rev{max(revision.value for revision in revisions)}", + release_title=release_title, + release_notes=release_notes, + ) def rock(): parser = argparse.ArgumentParser() parser.add_argument("--directory", required=True) - parser.add_argument("--create-release-tag", required=True) + parser.add_argument("--create-release", required=True) args = parser.parse_args() directory = pathlib.Path(args.directory) @@ -106,7 +144,7 @@ def rock(): "--format", "{{ .Digest }}", ] - ).strip() + ) logging.info(f"Uploading {rock_file=}") run( [ @@ -144,19 +182,14 @@ def rock(): .removeprefix("sha256:") ) - if json.loads(args.create_release_tag) is not True: + if json.loads(args.create_release) is not True: return - logging.info("Pushing git tag") - # Create git tag - tag = f"image-{multi_arch_digest}" - subprocess.run(["git", "tag", tag], check=True) - subprocess.run(["git", "push", "origin", tag], check=True) - # Output GitHub release info - github_actions.output["release_tag"] = tag - github_actions.output["release_title"] = f"Image {multi_arch_digest}" - release_notes = f"Released to {multi_arch_image_name}" - with open("release_notes.txt", "w") as file: - file.write(release_notes) + create_tags_and_release( + tags=[f"image-{multi_arch_digest}"], + release_tag=f"image-{multi_arch_digest}", + release_title=f"Image {multi_arch_digest}", + release_notes=f"Released to {multi_arch_image_name}", + ) def charm(): @@ -168,7 +201,7 @@ def charm(): parser = argparse.ArgumentParser() parser.add_argument("--directory", required=True) parser.add_argument("--channel", required=True) - parser.add_argument("--create-release-tags", required=True) + parser.add_argument("--create-release", required=True) args = parser.parse_args() directory = pathlib.Path(args.directory) @@ -224,25 +257,19 @@ def charm(): command += ["--resource", f"{oci.resource_name}:{oci.revision}"] run(command) - if json.loads(args.create_release_tags) is not True: + if json.loads(args.create_release) is not True: return - # Create git tags - logging.info("Pushing git tag(s)") - for revision in charm_revisions: - tag = f"rev{revision}" - subprocess.run(["git", "tag", tag], check=True) - subprocess.run(["git", "push", "origin", tag], check=True) - # Output GitHub release info - release_tag = f"rev{max(charm_revisions)}" - github_actions.output["release_tag"] = release_tag if len(charm_revisions) == 1: release_title = "Revision " else: release_title = "Revisions " release_title += ", ".join(str(revision) for revision in charm_revisions) - github_actions.output["release_title"] = release_title release_notes = f"Released to {args.channel}\nOCI images:\n" + "\n".join( f"- {dataclasses.asdict(oci)}" for oci in oci_resources ) - with open("release_notes.txt", "w") as file: - file.write(release_notes) + create_tags_and_release( + tags=[f"rev{revision}" for revision in charm_revisions], + release_tag=f"rev{max(charm_revisions)}", + release_title=release_title, + release_notes=release_notes, + )