funkwhale/scripts/build_metadata.py

255 wiersze
7.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Build metadata is a script that will extract build information from the environment,
either from CI variables or from git commands.
The build information are then mapped to the output format you need. The default output
format will print the build information as json.
- Release tags are stable version tags (e.g. 1.2.9),
- Prerelease tags are unstable version tags (e.g. 1.3.0-rc3),
- Development branches are both the 'stable' and 'develop' branches,
- Feature branches are any branch that will be merged in the development branches.
"""
import json
import logging
import os
import shlex
from argparse import ArgumentParser
from subprocess import check_output
from typing import List, Optional, TypedDict
from packaging.version import Version
logger = logging.getLogger(__name__)
PROJECT_NAME = "Funkwhale"
PROJECT_DESCRIPTION = "Funkwhale platform"
AUTHORS = "Funkwhale Collective"
WEBSITE_URL = "https://funkwhale.audio/"
SOURCE_URL = "https://dev.funkwhale.audio/funkwhale/funkwhale"
DOCUMENTATION_URL = "https://docs.funkwhale.audio"
LICENSE = "AGPL-3.0"
class Metadata(TypedDict):
commit_tag: str
commit_branch: str
commit_sha: str
commit_timestamp: str
commit_ref_name: str
version: str
"""
Version is:
- on release tags, the current tag name,
- on prerelease tags, the current tag name,
- on development branches, the latest tag name in the branch and the commit sha suffix,
- on feature branches, an empty string.
"""
tags: List[str]
"""
Tags are:
- on release tags, the current tag name and aliases in the form 'X.Y.Z', 'X.Y', 'X' and 'latest',
- on prerelease tags, the current tag name,
- on development branches, the current commit branch name,
- on feature branches, an empty list.
"""
latest: bool
"""
Latest is true when the current tag name is not a prerelease:
- on release tags: true,
- on prerelease tags: false,
- on development branches: false,
- on feature branches: false.
"""
def sh(cmd: str, **kwargs):
logger.debug("running command: %s", cmd)
return check_output(shlex.split(cmd), text=True, **kwargs).strip()
def latest_tag_on_branch() -> str:
"""
Return the latest tag on the current branch.
"""
return sh("git describe --tags --abbrev=0")
def env_or_cmd(key: str, cmd: str) -> str:
if "CI" in os.environ:
return os.environ.get(key, "")
return sh(cmd)
def extract_metadata() -> Metadata:
commit_tag = env_or_cmd(
"CI_COMMIT_TAG",
"git tag --points-at HEAD",
)
commit_branch = env_or_cmd(
"CI_COMMIT_BRANCH",
"git rev-parse --abbrev-ref HEAD",
)
commit_sha = env_or_cmd(
"CI_COMMIT_SHA",
"git rev-parse HEAD",
)
commit_timestamp = env_or_cmd(
"CI_COMMIT_TIMESTAMP",
"git show -s --format=%cI HEAD",
)
commit_ref_name = os.environ.get(
"CI_COMMIT_REF_NAME",
default=commit_tag or commit_branch,
)
logger.info("found commit_tag: %s", commit_tag)
logger.info("found commit_branch: %s", commit_branch)
logger.info("found commit_sha: %s", commit_sha)
logger.info("found commit_timestamp: %s", commit_timestamp)
logger.info("found commit_ref_name: %s", commit_ref_name)
version = ""
tags = []
latest = False
if commit_tag: # Tagged version
version = Version(commit_tag)
if version.is_prerelease:
logger.info("build is for a prerelease tag")
tags.append(commit_tag)
else:
logger.info("build is for a release tag")
tags.append(f"{version.major}.{version.minor}.{version.micro}")
tags.append(f"{version.major}.{version.minor}")
tags.append(f"{version.major}")
tags.append("latest")
latest = True
version = tags[0]
else: # Branch version
if commit_branch in ("stable", "develop"):
logger.info("build is for a development branch")
tags.append(commit_branch)
previous_tag = latest_tag_on_branch()
previous_version = Version(previous_tag)
version = f"{previous_version.base_version}-dev+{commit_sha[:7]}"
else:
logger.info("build is for a feature branch")
return {
"commit_tag": commit_tag,
"commit_branch": commit_branch,
"commit_sha": commit_sha,
"commit_timestamp": commit_timestamp,
"commit_ref_name": commit_ref_name,
"version": version,
"tags": tags,
"latest": latest,
}
def bake_output(
metadata: Metadata,
target: Optional[str],
images: Optional[List[str]],
) -> dict:
if target is None:
logger.error("no bake target provided, exiting...")
raise SystemExit(1)
if images is None:
logger.error("no bake images provided, exiting...")
raise SystemExit(1)
docker_tags = [f"{img}:{tag}" for img in images for tag in metadata["tags"]]
docker_labels = {
"org.opencontainers.image.title": PROJECT_NAME,
"org.opencontainers.image.description": PROJECT_DESCRIPTION,
"org.opencontainers.image.url": WEBSITE_URL,
"org.opencontainers.image.source": SOURCE_URL,
"org.opencontainers.image.documentation": DOCUMENTATION_URL,
"org.opencontainers.image.licenses": LICENSE,
"org.opencontainers.image.vendor": AUTHORS,
"org.opencontainers.image.version": metadata["commit_ref_name"],
"org.opencontainers.image.created": metadata["commit_timestamp"],
"org.opencontainers.image.revision": metadata["commit_sha"],
}
return {
"target": {
target: {
"tags": docker_tags,
"labels": docker_labels,
}
}
}
def env_output(metadata: Metadata) -> list[str]:
env_dict = {
"BUILD_COMMIT_TAG": str(metadata["commit_tag"]),
"BUILD_COMMIT_BRANCH": str(metadata["commit_branch"]),
"BUILD_COMMIT_SHA": str(metadata["commit_sha"]),
"BUILD_COMMIT_TIMESTAMP": str(metadata["commit_timestamp"]),
"BUILD_COMMIT_REF_NAME": str(metadata["commit_ref_name"]),
"BUILD_VERSION": str(metadata["version"]),
"BUILD_TAGS": ",".join(metadata["tags"]),
"BUILD_LATEST": str(metadata["latest"]).lower(),
}
return [f"{key}={value}" for key, value in env_dict.items()]
def main(
format_: str,
bake_target: Optional[str],
bake_images: Optional[List[str]],
) -> int:
metadata = extract_metadata()
if format_ == "bake":
result = json.dumps(
bake_output(metadata=metadata, target=bake_target, images=bake_images),
indent=2,
)
elif format_ == "env":
result = "\n".join(env_output(metadata=metadata))
else:
result = json.dumps(metadata, indent=2)
print(result)
return 0
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument(
"-f",
"--format",
choices=["bake", "env"],
default=None,
help="Print format for the metadata",
)
parser.add_argument(
"--bake-target",
help="Target for the bake metadata",
)
parser.add_argument(
"--bake-image",
action="append",
dest="bake_images",
help="Image names for the bake metadata",
)
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
raise SystemExit(main(args.format, args.bake_target, args.bake_images))