Skip to content

Commit

Permalink
compatible: Add release_charm.yaml (#20)
Browse files Browse the repository at this point in the history
## Issue

[canonical/charming-actions/upload-charm](https://github.com/canonical/charming-actions/tree/main/upload-charm)
does not support multiple series

[DPE-1273](https://warthogs.atlassian.net/browse/DPE-1273)

Also resolves [design
bug](https://chat.canonical.com/canonical/pl/sjwya3bo3fd75ni4zbk5caqxsw)
## Solution
Replace charming-actions/upload-charm with a reusable workflow + Python
script (so it's easier to maintain)
  • Loading branch information
carlcsaposs-canonical authored Mar 23, 2023
1 parent c0da434 commit dfee76d
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 5 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/_get_workflow_version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,16 @@ jobs:
try:
os.makedirs(repository_name)
except FileExistsError:
pass
commands = [f"git checkout {ref}"]
else:
commands = [
"git init",
"git sparse-checkout set --sparse-index .github/workflows/",
f"git remote add --fetch origin https://github.com/{repository_name}.git",
f"git pull origin {ref}",
f"git checkout {ref}",
]
for command in commands:
subprocess.check_output(command.split(" "), cwd=repository_name)
for command in commands:
subprocess.check_output(command.split(" "), cwd=repository_name)
jobs = yaml.safe_load(caller_workflow_file_path.read_text())["jobs"]
versions_ = set()
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/build_charm_without_cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Workflow file: [build_charm_without_cache.yaml](build_charm_without_cache.yaml)

## Usage
```yaml
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
jobs:
build:
name: Build charm
uses: canonical/data-platform-workflows/.github/workflows/build_charm_without_cache.yaml@v2
```
59 changes: 59 additions & 0 deletions .github/workflows/build_charm_without_cache.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

# Usage documentation: build_charm_without_cache.md

on:
workflow_call:
inputs:
artifact-name:
description: Packed charm is uploaded to this GitHub artifact name
default: charm-packed-without-cache
type: string
path-to-charm-directory:
description: Relative path to charm directory from repository directory
default: .
type: string
outputs:
artifact-name:
description: Packed charm is uploaded to this GitHub artifact name
value: ${{ inputs.artifact-name }}

jobs:
build:
name: Build charm
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
# TODO: Replace with custom image on self-hosted runner
run: |
# Copied from https://github.com/charmed-kubernetes/actions-operator/blob/96fb0b07eb675f74cf1796be812bc7e67a0d62fc/src/bootstrap/index.ts#L151
sudo adduser $USER lxd
newgrp lxd
sudo lxd waitready
sudo lxd init --auto
sudo iptables -F FORWARD
sudo iptables -P FORWARD ACCEPT
sudo chmod a+wr /var/snap/lxd/common/lxd/unix.socket
sudo snap install charmcraft --classic
- name: Pack charm
id: pack
working-directory: ${{ inputs.path-to-charm-directory }}
run: charmcraft pack
- name: Upload charmcraft logs
if: ${{ failure() && steps.pack.outcome == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifact-name }}-charmcraft-build-logs
path: ~/.local/state/charmcraft/log/
if-no-files-found: error
- name: Upload packed charm
uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifact-name }}
path: |
${{ inputs.path-to-charm-directory }}/*.charm
if-no-files-found: error
2 changes: 1 addition & 1 deletion .github/workflows/build_charms_with_cache.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ jobs:
if: ${{ failure() && steps.pack.outcome == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifact-name }}-charmcraft-logs
name: ${{ inputs.artifact-name }}-charmcraft-build-logs
path: ~/.local/state/charmcraft/log/
if-no-files-found: error
- run: touch .empty
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/release_charm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Workflow file: [release_charm.yaml](release_charm.yaml)

## Usage
Add `.yaml` file to `.github/workflows/`
```yaml
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
name: Release to Charmhub

on:
push:
branches:
- main

jobs:
build:
name: Build charm
uses: canonical/data-platform-workflows/.github/workflows/build_charm_without_cache.yaml@v2

release:
name: Release charm
needs:
- build
uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v2
with:
channel: latest/edge
artifact-name: ${{ needs.build.outputs.artifact-name }}
secrets:
charmhub-token: ${{ secrets.CHARMHUB_TOKEN }}
permissions:
contents: write # Needed to create GitHub release
```
90 changes: 90 additions & 0 deletions .github/workflows/release_charm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

# Usage documentation: release_charm.md

on:
workflow_call:
inputs:
channel:
description: Charmhub channel to release to
required: true
type: string
artifact-name:
description: Name of GitHub artifact that contains the .charm file(s) (use another workflow to build the charm)
required: true
type: string
path-to-charm-directory:
description: Relative path to charm directory (from GitHub artifact root)
default: .
type: string
create-github-release:
description: Create git tag & GitHub release
default: true
type: boolean
secrets:
charmhub-token:
description: Charmhub login token
required: true

jobs:
get-workflow-version:
name: Get workflow version
uses: ./.github/workflows/_get_workflow_version.yaml
with:
repository-name: canonical/data-platform-workflows
file-name: release_charm.yaml

release-charm:
name: Release charm
needs:
- get-workflow-version
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Install pyyaml
run: python3 -m pip install pyyaml
- name: Install docker
run: |
sudo adduser $USER docker
newgrp docker
sudo snap install docker
- name: Install charmcraft
run: sudo snap install charmcraft --classic
- name: Checkout caller repository
uses: actions/checkout@v3
with:
path: caller-repo
- name: Checkout release workflow repository
uses: actions/checkout@v3
with:
repository: canonical/data-platform-workflows
path: workflow-repo
ref: ${{ needs.get-workflow-version.outputs.version }}
- name: Download .charm file(s)
uses: actions/download-artifact@v3
with:
name: ${{ inputs.artifact-name }}
path: caller-repo
- name: Upload & release charm
id: release
working-directory: caller-repo
run: python3 ../workflow-repo/release_charm.py --charm-directory ${{ inputs.path-to-charm-directory }} --channel ${{ inputs.channel }}
env:
CHARMCRAFT_AUTH: ${{ secrets.charmhub-token }}
- name: Create GitHub release
if: ${{ inputs.create-github-release }}
working-directory: caller-repo
run: |
git tag ${{ steps.release.outputs.release_tag }}
git push origin ${{ steps.release.outputs.release_tag }}
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 }}
- name: Upload charmcraft logs
if: ${{ failure() && steps.release.outcome == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifact-name }}-charmcraft-release-logs
path: ~/.local/state/charmcraft/log/
if-no-files-found: error
114 changes: 114 additions & 0 deletions release_charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import argparse
import dataclasses
import json
import logging
import os
import pathlib
import subprocess
import sys

import yaml


@dataclasses.dataclass
class OCIResource:
"""OCI image that has been uploaded to Charmhub as a charm resource"""

resource_name: str
revision: int


def run(command_: list):
"""Run subprocess command & log stderr
Returns:
stdout
"""
process = subprocess.run(command_, capture_output=True, encoding="utf-8")
try:
process.check_returncode()
except subprocess.CalledProcessError as e:
logging.error(e.stderr)
raise
return process.stdout


logging.basicConfig(level=logging.INFO, stream=sys.stdout)
parser = argparse.ArgumentParser()
parser.add_argument("--charm-directory")
parser.add_argument("--channel")
args = parser.parse_args()
charm_directory = pathlib.Path(args.charm_directory)

# Upload charm file(s) & store revision
charm_revisions: list[int] = []
for charm_file in charm_directory.glob("*.charm"):
logging.info(f"Uploading {charm_file=}")
output = run(["charmcraft", "upload", "--format", "json", charm_file])
revision: int = json.loads(output)["revision"]
logging.info(f"Uploaded charm {revision=}")
charm_revisions.append(revision)
assert len(charm_revisions) > 0, "No .charm files found"

metadata_file = yaml.safe_load((charm_directory / "metadata.yaml").read_text())
charm_name = metadata_file["name"]

# (Only for Kubernetes charms) upload OCI image(s) & store revision
oci_resources: list[OCIResource] = []
resources = metadata_file.get("resources", {})
for resource_name, resource in resources.items():
if resource["type"] != "oci-image":
continue
image_name = resource["upstream-source"]
logging.info(f"Downloading OCI image: {image_name}")
run(["docker", "pull", image_name])
image_id = run(["docker", "image", "inspect", image_name, "--format", "'{{.Id}}'"])
image_id = image_id.rstrip("\n").strip("'").lstrip("sha256:")
assert "\n" not in image_id, f"Multiple local images found for {image_name}"
logging.info(f"Uploading charm resource: {resource_name}")
output = run(
[
"charmcraft",
"upload-resource",
"--format",
"json",
charm_name,
resource_name,
"--image",
image_id,
]
)
revision: int = json.loads(output)["revision"]
logging.info(f"Uploaded charm resource {revision=}")
oci_resources.append(OCIResource(resource_name, revision))

# Release charm file(s)
for charm_revision in charm_revisions:
logging.info(f"Releasing {charm_revision=}")
command = [
"charmcraft",
"release",
charm_name,
"--revision",
str(charm_revision),
"--channel",
args.channel,
]
for oci in oci_resources:
command += ["--resource", f"{oci.resource_name}:{oci.revision}"]
run(command)

# Output GitHub release info
release_tag = f"rev{max(charm_revisions)}"
if len(charm_revisions) == 1:
release_title = "Revision "
else:
release_title = "Revisions "
release_title += ", ".join(str(revision) for revision in charm_revisions)
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)
with open(os.environ["GITHUB_OUTPUT"], "a") as file:
file.write(f"release_tag={release_tag}\nrelease_title={release_title}")

0 comments on commit dfee76d

Please sign in to comment.