Uploading and releasing a package with resources

One of the most common workflows when communicating with the store API is to release a package. This guide explains how to upload and release a package with an associated resource. We will use a charm as an example.

This performs roughly the same store operations as running:

charmcraft upload-resource my-charm my-file --filepath cat.gif
charmcraft upload my-charm.charm
charmcraft release my-charm

Get a Charmhub client

Create a StoreClient instance that points to the staging instance of CharmHub:

base_url: str = "https://api.staging.charmhub.io"
storage_base_url: str = "https://storage.staging.snapcraftcontent.com"
client = StoreClient(
    application_name="craft-store-demo",
    base_url=base_url,
    storage_base_url=storage_base_url,
    endpoints=endpoints.CHARMHUB,
    user_agent="craft-store-demo-app",
    environment_auth="CRAFT_STORE_CHARMCRAFT_CREDENTIALS",
)

Push the resource

Next, you’ll need to:

  1. Upload the resource file.

  2. Connect that uploaded file with the charm resource.

  3. Poll the status while CharmHub processes the file.

  4. Retrieve the revision number assigned to the file.

resource_upload_id = client.upload_file(filepath=resource_path)
resource_status_url = client.push_resource(
    name=charm_name,
    resource_name=resource_name,
    upload_id=resource_upload_id,
    resource_type=CharmResourceType.FILE,
)
resource_status = check_status(client, base_url + resource_status_url)[0]
resource_revision = int(resource_status["revision"])

The snippet above uses a check_status helper function that polls CharmHub every three seconds while the file processes.

check_status function
def check_status(client: StoreClient, status_url: str) -> list[dict[str, Any]]:
    """Check the status of an upload."""
    timeout = time.monotonic() + 120
    while time.monotonic() < timeout:
        resource_status = client.request("GET", status_url).json()
        done = True
        for resource_revision_status in resource_status["revisions"]:
            if resource_revision_status["status"] not in ("approved", "rejected"):
                done = False
        if done:
            return resource_status["revisions"]
        time.sleep(3)
    raise TimeoutError("Status was neither approved nor rejected after 120s")

This demo uploads the file silently, but the upload progress can also be monitored interactively through a callback, as demonstrated in Using craft-cli for upload progress. Likewise, polling the resource’s status URL may be done in other (perhaps more user-friendly) ways.

For Charmhub, resources may optionally include a list of bases.

Push the package

This next segment is very similar, as it:

  1. Uploads the charm

  2. Connects that uploaded file with the charm revision

  3. Polls Charmhub while it processes the charm.

  4. Retrieves the revision number assigned to the charm upload.

charm_upload_id = client.upload_file(filepath=charm_path)
charm_status_url = client.notify_revision(
    name=charm_name,
    revision_request=RevisionsRequestModel(upload_id=charm_upload_id),
).status_url
charm_status = check_status(client, base_url + charm_status_url)[0]
charm_revision = charm_status["revision"]

Release the Kraken

Now that the charm and its resource have been uploaded, they can be released to a channel. Upon release, the revision is tied to the relevant resources.

client.release(
    name=charm_name,
    release_request=[
        ReleaseRequestModel(
            channel="edge",
            resources=[
                ResourceModel(name=resource_name, revision=resource_revision)
            ],
            revision=charm_revision,
        )
    ],
)

Below is a full file containing an executable version of the script in this guide.

Full example
#!/usr/bin/env python3
"""Demo code for uploading a charm to Charmhub."""
import argparse
import pathlib
import sys
import time
from typing import Any

from craft_store import endpoints, StoreClient
from craft_store.models import (
    RevisionsRequestModel,
    ReleaseRequestModel,
    ResourceModel,
    CharmResourceType,
)


def parse_args(argv: list[str]):
    parser = argparse.ArgumentParser(
        prog="upload_package",
        description="Uploads a charm and its resource",
    )
    parser.add_argument(
        "charm_name",
        help="The name of the charm in the store",
    )
    parser.add_argument(
        "resource_name",
        help="The name of the resource in the store",
    )
    parser.add_argument(
        "--charm",
        type=pathlib.Path,
        required=True,
        help="The path of the charm to upload",
    )
    parser.add_argument(
        "--resource",
        type=pathlib.Path,
        required=True,
        help="The path of the resource file to upload",
    )
    return parser.parse_args(argv)


def check_status(client: StoreClient, status_url: str) -> list[dict[str, Any]]:
    """Check the status of an upload."""
    timeout = time.monotonic() + 120
    while time.monotonic() < timeout:
        resource_status = client.request("GET", status_url).json()
        done = True
        for resource_revision_status in resource_status["revisions"]:
            if resource_revision_status["status"] not in ("approved", "rejected"):
                done = False
        if done:
            return resource_status["revisions"]
        time.sleep(3)
    raise TimeoutError("Status was neither approved nor rejected after 120s")


def main(argv: list[str]):
    args = parse_args(argv)
    charm_path = args.charm.expanduser().resolve()
    resource_path = args.resource.expanduser().resolve()
    charm_name = args.charm_name
    resource_name = args.resource_name

    # [docs:get-client]
    base_url: str = "https://api.staging.charmhub.io"
    storage_base_url: str = "https://storage.staging.snapcraftcontent.com"
    client = StoreClient(
        application_name="craft-store-demo",
        base_url=base_url,
        storage_base_url=storage_base_url,
        endpoints=endpoints.CHARMHUB,
        user_agent="craft-store-demo-app",
        environment_auth="CRAFT_STORE_CHARMCRAFT_CREDENTIALS",
    )
    # [docs:get-client-end]
    # [docs:upload-resource]
    resource_upload_id = client.upload_file(filepath=resource_path)
    resource_status_url = client.push_resource(
        name=charm_name,
        resource_name=resource_name,
        upload_id=resource_upload_id,
        resource_type=CharmResourceType.FILE,
    )
    resource_status = check_status(client, base_url + resource_status_url)[0]
    resource_revision = int(resource_status["revision"])
    # [docs:upload-resource-end]
    # [docs:upload-charm]
    charm_upload_id = client.upload_file(filepath=charm_path)
    charm_status_url = client.notify_revision(
        name=charm_name,
        revision_request=RevisionsRequestModel(upload_id=charm_upload_id),
    ).status_url
    charm_status = check_status(client, base_url + charm_status_url)[0]
    charm_revision = charm_status["revision"]
    # [docs:upload-charm-end]

    # [docs:release]
    client.release(
        name=charm_name,
        release_request=[
            ReleaseRequestModel(
                channel="edge",
                resources=[
                    ResourceModel(name=resource_name, revision=resource_revision)
                ],
                revision=charm_revision,
            )
        ],
    )
    # [docs:release-end]


if __name__ == "__main__":
    main(sys.argv[1:])

You can run it as such:

  python3 upload_package.py \
    --charm=test.charm \
    --resource=empty-file \
    $my_charm \
    my-file