From 4f3493cd34b520ab3769e05145c1ed4107c76e3a Mon Sep 17 00:00:00 2001 From: Yasen Date: Thu, 19 Mar 2026 15:58:36 +0100 Subject: [PATCH 1/4] tests --- .ci/container_setup.d/README | 7 ++ .ci/gen_certs.py | 62 +++++++++ .ci/nginx.conf.j2 | 167 +++++++++++++++++++++++++ .ci/run_container.sh | 126 +++++++++++++++++++ .ci/scripts/calc_constraints.py | 119 ++++++++++++++++++ .ci/scripts/check_cli_dependencies.py | 36 ++++++ .ci/scripts/check_click_for_mypy.py | 18 +++ .ci/scripts/collect_changes.py | 115 +++++++++++++++++ .ci/scripts/create_release_branch.sh | 33 +++++ .ci/scripts/pr_labels.py | 63 ++++++++++ .ci/scripts/release.sh | 26 ++++ .ci/scripts/validate_commit_message.py | 75 +++++++++++ .ci/settings/settings.py | 59 +++++++++ .github/workflows/build.yml | 38 ++++++ .github/workflows/codeql.yml | 31 +++++ .github/workflows/collect_changes.yml | 42 +++++++ .github/workflows/cookiecutter.yml | 107 ++++++++++++++++ .github/workflows/lint.yml | 39 ++++++ .github/workflows/nightly.yml | 21 ++++ .github/workflows/pr.yml | 68 ++++++++++ .github/workflows/pr_checks.yml | 60 +++++++++ .github/workflows/publish.yml | 39 ++++++ .github/workflows/release.yml | 30 +++++ .github/workflows/release_branch.yml | 56 +++++++++ .github/workflows/test.yml | 57 +++++++++ .gitignore | 8 ++ CHANGES/.TEMPLATE.md | 42 +++++++ CHANGES/pulp-glue-npm/.gitkeep | 0 Makefile | 55 ++++++++ lint_requirements.txt | 14 +++ pulp-glue-npm/pyproject.toml | 36 ++++++ pyproject.toml | 156 ++++++++++++++++++++++- releasing.md | 20 +++ tests/scripts/config.source | 107 ++++++++++++++++ 34 files changed, 1931 insertions(+), 1 deletion(-) create mode 100644 .ci/container_setup.d/README create mode 100644 .ci/gen_certs.py create mode 100644 .ci/nginx.conf.j2 create mode 100755 .ci/run_container.sh create mode 100755 .ci/scripts/calc_constraints.py create mode 100755 .ci/scripts/check_cli_dependencies.py create mode 100755 .ci/scripts/check_click_for_mypy.py create mode 100755 .ci/scripts/collect_changes.py create mode 100755 .ci/scripts/create_release_branch.sh create mode 100755 .ci/scripts/pr_labels.py create mode 100755 .ci/scripts/release.sh create mode 100644 .ci/scripts/validate_commit_message.py create mode 100644 .ci/settings/settings.py create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/collect_changes.yml create mode 100644 .github/workflows/cookiecutter.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/nightly.yml create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/pr_checks.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/release_branch.yml create mode 100644 .gitignore create mode 100644 CHANGES/.TEMPLATE.md create mode 100644 CHANGES/pulp-glue-npm/.gitkeep create mode 100644 Makefile create mode 100644 lint_requirements.txt create mode 100644 releasing.md create mode 100644 tests/scripts/config.source diff --git a/.ci/container_setup.d/README b/.ci/container_setup.d/README new file mode 100644 index 0000000..9604335 --- /dev/null +++ b/.ci/container_setup.d/README @@ -0,0 +1,7 @@ +Files in this directory of the form '-.sh' are executed in alphabetical order. +They can assume to be provided with the following environmnent variables: +* PULP_CLI_CONFIG a path to a config file for the ci container +* CONTAINER_RUNTIME the command for interacting with containers +* BASE_PATH the directory the 'run_container.sh' script lives in + +Also a running container named 'pulp-ephemeral'. diff --git a/.ci/gen_certs.py b/.ci/gen_certs.py new file mode 100644 index 0000000..d55b0b9 --- /dev/null +++ b/.ci/gen_certs.py @@ -0,0 +1,62 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "trustme>=1.2.1,<1.3.0", +# ] +# /// + +import argparse +import os +import sys + +import trustme + + +def main() -> None: + parser = argparse.ArgumentParser(prog="gen_certs") + parser.add_argument( + "-d", + "--dir", + default=os.getcwd(), + help="Directory where certificates and keys are written to. Defaults to cwd.", + ) + + args = parser.parse_args(sys.argv[1:]) + cert_dir = args.dir + + if not os.path.isdir(cert_dir): + raise ValueError(f"--dir={cert_dir} is not a directory") + + key_type = trustme.KeyType["ECDSA"] + + # Generate the CA certificate + ca = trustme.CA(key_type=key_type) + # Write the certificate the client should trust + ca_cert_path = os.path.join(cert_dir, "ca.pem") + ca.cert_pem.write_to_path(path=ca_cert_path) + + # Generate the server certificate + server_cert = ca.issue_cert("localhost", "127.0.0.1", "::1", key_type=key_type) + # Write the certificate and private key the server should use + server_key_path = os.path.join(cert_dir, "server.key") + server_cert_path = os.path.join(cert_dir, "server.pem") + server_cert.private_key_pem.write_to_path(path=server_key_path) + with open(server_cert_path, mode="w") as f: + f.truncate() + for blob in server_cert.cert_chain_pems: + blob.write_to_path(path=server_cert_path, append=True) + + # Generate the client certificate + client_cert = ca.issue_cert("admin@example.com", common_name="admin", key_type=key_type) + # Write the certificate and private key the client should use + client_key_path = os.path.join(cert_dir, "client.key") + client_cert_path = os.path.join(cert_dir, "client.pem") + client_cert.private_key_pem.write_to_path(path=client_key_path) + with open(client_cert_path, mode="w") as f: + f.truncate() + for blob in client_cert.cert_chain_pems: + blob.write_to_path(path=client_cert_path, append=True) + + +if __name__ == "__main__": + main() diff --git a/.ci/nginx.conf.j2 b/.ci/nginx.conf.j2 new file mode 100644 index 0000000..cf3695e --- /dev/null +++ b/.ci/nginx.conf.j2 @@ -0,0 +1,167 @@ +# Copy from pulp-oci-images. +# Ideally we can get it upstream again. +# +# The "nginx" package on fedora creates this user and group. +user nginx nginx; +# Gunicorn docs suggest this value. +worker_processes 1; +daemon off; +events { + worker_connections 1024; # increase if you have lots of clients + accept_mutex off; # set to 'on' if nginx worker_processes > 1 +} + +http { + include mime.types; + # fallback in case we can't determine a type + default_type application/octet-stream; + sendfile on; + + # If left at the default of 1024, nginx emits a warning about being unable + # to build optimal hash types. + types_hash_max_size 4096; + + {%- if https | default(false) %} + map $ssl_client_s_dn $ssl_client_s_dn_cn { + default ""; + ~CN=(?[^,]+) $CN; + } + {%- endif %} + + upstream pulp-content { + server 127.0.0.1:24816; + } + + upstream pulp-api { + server 127.0.0.1:24817; + } + + server { + # Gunicorn docs suggest the use of the "deferred" directive on Linux. + {% if https | default(false) -%} + listen 443 default_server deferred ssl; + + ssl_certificate /etc/pulp/certs/pulp_webserver.crt; + ssl_certificate_key /etc/pulp/certs/pulp_webserver.key; + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # intermediate configuration + ssl_protocols TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; + ssl_prefer_server_ciphers on; + + # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) + add_header Strict-Transport-Security max-age=15768000; + + # Configure client cert authentication + ssl_client_certificate /etc/pulp/certs/ca.pem; + ssl_verify_client optional; + {%- else -%} + listen 80 default_server deferred; + {%- endif %} + server_name $hostname; + + # The default client_max_body_size is 1m. Clients uploading + # files larger than this will need to chunk said files. + client_max_body_size 10m; + + # Gunicorn docs suggest this value. + keepalive_timeout 5; + + location {{ content_path }} { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://pulp-content; + } + + location {{ api_root }}api/v3/ { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + {%- if https | default(false) %} + proxy_set_header Remoteuser $ssl_client_s_dn_cn; + {%- endif %} + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://pulp-api; + client_max_body_size 0; + } + + {%- if domain_enabled | default(false) %} + location ~ {{ api_root }}.+/api/v3/ { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://pulp-api; + client_max_body_size 0; + } + {%- endif %} + + location /auth/login/ { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://pulp-api; + } + + include pulp/*.conf; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://pulp-api; + # most pulp static files are served through whitenoise + # http://whitenoise.evans.io/en/stable/ + } + + {%- if https | default(false) %} + # ACME http-01 tokens, i.e, for Let's Encrypt + location /.well-known/ { + try_files $uri $uri/ =404; + } + {%- endif %} + {% if https | default(false) -%} + location /oauth2token/ { + auth_basic "Tokens, Tokens, Tokens"; + auth_basic_user_file /etc/pulp/certs/oauth2passwd; + if ($request_method !~ POST) { + # This still triggers earlier than the auth_basic in the outer block. + return 403; + } + try_files /dev/null @oauth2token; + } + # Nginx "return" kicks in before basic_auth, so we must use it in a separate block. + # https://stackoverflow.com/questions/67975464/why-doesnt-basic-auth-work-with-a-simple-nginx-return-statement + location @oauth2token { + default_type application/json; + charset utf-8; + + return 200 '{"access_token": "DEADBEEF", "token_type": "bearer", "expires_in": 30}'; + } + {%- endif %} + } + {%- if https | default(false) %} + server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; + } + {%- endif %} +} diff --git a/.ci/run_container.sh b/.ci/run_container.sh new file mode 100755 index 0000000..b3a1c7e --- /dev/null +++ b/.ci/run_container.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +set -eu + +BASEPATH="$(dirname "$(readlink -f "$0")")" +export BASEPATH + +if [ -z "${CONTAINER_RUNTIME:+x}" ] +then + if ls /usr/bin/podman + then + CONTAINER_RUNTIME=podman + else + CONTAINER_RUNTIME=docker + fi +fi +export CONTAINER_RUNTIME + +PULP_CLI_TEST_TMPDIR="$(mktemp -d)" +export PULP_CLI_TEST_TMPDIR + +cleanup () { + "${CONTAINER_RUNTIME}" stop pulp-ephemeral && true + rm -rf "${PULP_CLI_TEST_TMPDIR}" +} + +trap cleanup EXIT +trap cleanup INT + +if [ -z "${KEEP_CONTAINER:+x}" ] +then + RM="yes" +else + RM="" +fi + +IMAGE_TAG="${IMAGE_TAG:-latest}" +FROM_TAG="${FROM_TAG:-latest}" + +if [ "${CONTAINER_FILE:+x}" ] +then + IMAGE_TAG="ephemeral-build" + "${CONTAINER_RUNTIME}" build --file "${BASEPATH}/assets/${CONTAINER_FILE}" --build-arg FROM_TAG="${FROM_TAG}" --tag ghcr.io/pulp/pulp:"${IMAGE_TAG}" . +fi + +if [ "$(getenforce)" = "Enforcing" ]; then + SELINUX="yes" +else + SELINUX="" +fi; + +mkdir -p "${PULP_CLI_TEST_TMPDIR}/settings/certs" +cp "${BASEPATH}/settings/settings.py" "${PULP_CLI_TEST_TMPDIR}/settings/settings.py" +echo "service_acct:$(openssl passwd secret)" > "${PULP_CLI_TEST_TMPDIR}/settings/certs/oauth2passwd" + +if [ -z "${PULP_HTTPS:+x}" ] +then + PROTOCOL="http" + PORT="80" + PULP_CONTENT_ORIGIN="http://localhost:8080/" +else + PROTOCOL="https" + PORT="443" + PULP_CONTENT_ORIGIN="https://localhost:8080/" + python3 "${BASEPATH}/gen_certs.py" -d "${PULP_CLI_TEST_TMPDIR}/settings/certs" + export PULP_CA_BUNDLE="${PULP_CLI_TEST_TMPDIR}/settings/certs/ca.pem" + ln -fs server.pem "${PULP_CLI_TEST_TMPDIR}/settings/certs/pulp_webserver.crt" + ln -fs server.key "${PULP_CLI_TEST_TMPDIR}/settings/certs/pulp_webserver.key" +fi +export PULP_CONTENT_ORIGIN + +"${CONTAINER_RUNTIME}" \ + run ${RM:+--rm} \ + --env S6_KEEP_ENV=1 \ + ${OAS_VERSION:+--env PULP_SPECTACULAR_SETTINGS__OAS_VERSION="${OAS_VERSION}"} \ + ${PULP_HTTPS:+--env PULP_HTTPS} \ + ${PULP_OAUTH2:+--env PULP_OAUTH2} \ + ${PULP_API_ROOT:+--env PULP_API_ROOT} \ + ${PULP_DOMAIN_ENABLED:+--env PULP_DOMAIN_ENABLED} \ + ${PULP_ENABLED_PLUGINS:+--env PULP_ENABLED_PLUGINS} \ + --env PULP_CONTENT_ORIGIN \ + --detach \ + --name "pulp-ephemeral" \ + --volume "${PULP_CLI_TEST_TMPDIR}/settings:/etc/pulp${SELINUX:+:Z}" \ + --volume "${BASEPATH}/nginx.conf.j2:/nginx/nginx.conf.j2${SELINUX:+:Z}" \ + --network bridge \ + --publish "8080:${PORT}" \ + "ghcr.io/pulp/pulp:${IMAGE_TAG}" + +echo "Wait for pulp to start." +for counter in $(seq 40 -1 0) +do + if [ "$counter" = "0" ] + then + echo "FAIL." + "${CONTAINER_RUNTIME}" images + "${CONTAINER_RUNTIME}" ps -a + "${CONTAINER_RUNTIME}" logs "pulp-ephemeral" + exit 1 + fi + + sleep 3 + if curl --insecure --fail "${PROTOCOL}://localhost:8080${PULP_API_ROOT:-/pulp/}api/v3/status/" > /dev/null 2>&1 + then + echo "SUCCESS." + break + fi + echo "." +done + +# Set admin password +"${CONTAINER_RUNTIME}" exec "pulp-ephemeral" pulpcore-manager reset-admin-password --password password + +# Create pulp config +PULP_CLI_CONFIG="${PULP_CLI_TEST_TMPDIR}/settings/certs/cli.toml" +export PULP_CLI_CONFIG +pulp config create --overwrite --location "${PULP_CLI_CONFIG}" --base-url "${PROTOCOL}://localhost:8080" ${PULP_API_ROOT:+--api-root "${PULP_API_ROOT}"} --username "admin" --password "password" +# show pulpcore/plugin versions we're using +pulp --config "${PULP_CLI_CONFIG}" --refresh-api status + +if [ -d "${BASEPATH}/container_setup.d/" ] +then + run-parts --exit-on-error --regex '^[0-9]+-[-_[:alnum:]]*\.sh$' "${BASEPATH}/container_setup.d/" +fi + +PULP_LOGGING="${CONTAINER_RUNTIME}" "$@" diff --git a/.ci/scripts/calc_constraints.py b/.ci/scripts/calc_constraints.py new file mode 100755 index 0000000..ca8e11e --- /dev/null +++ b/.ci/scripts/calc_constraints.py @@ -0,0 +1,119 @@ +#!/bin/python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "packaging>=25.0,<25.1", +# "tomli>=2.3.0,<2.4.0;python_version<'3.11'", +# ] +# /// + +import argparse +import fileinput +import sys + +from packaging.requirements import Requirement +from packaging.version import Version + +try: + import tomllib +except ImportError: + import tomli as tomllib + + +def split_comment(line): + split_line = line.split("#", maxsplit=1) + try: + comment = " # " + split_line[1].strip() + except IndexError: + comment = "" + return split_line[0].strip(), comment + + +def to_upper_bound(req): + try: + requirement = Requirement(req) + except ValueError: + return f"# UNPARSABLE: {req}" + else: + for spec in requirement.specifier: + if spec.operator == "~=": + return f"# NO BETTER CONSTRAINT: {req}" + if spec.operator == "<=": + operator = "==" + max_version = spec.version + return f"{requirement.name}{operator}{max_version}" + if spec.operator == "<": + operator = "~=" + version = Version(spec.version) + if version.micro != 0: + max_version = f"{version.major}.{version.minor}.{version.micro - 1}" + elif version.minor != 0: + max_version = f"{version.major}.{version.minor - 1}" + elif version.major != 0: + max_version = f"{version.major - 1}.0" + else: + return f"# NO BETTER CONSTRAINT: {req}" + return f"{requirement.name}{operator}{max_version}" + return f"# NO UPPER BOUND: {req}" + + +def to_lower_bound(req): + try: + requirement = Requirement(req) + except ValueError: + return f"# UNPARSABLE: {req}" + else: + for spec in requirement.specifier: + if spec.operator == ">=": + if requirement.name == "pulpcore": + # Currently an exception to allow for pulpcore bugfix releases. + # TODO Semver libraries should be allowed too. + operator = "~=" + else: + operator = "==" + min_version = spec.version + return f"{requirement.name}{operator}{min_version}" + return f"# NO LOWER BOUND: {req}" + + +def main(): + """Calculate constraints for the lower bound of dependencies where possible.""" + parser = argparse.ArgumentParser( + prog=sys.argv[0], + description="Calculate constraints for the lower or upper bound of dependencies where " + "possible.", + ) + parser.add_argument("-u", "--upper", action="store_true") + parser.add_argument("filename", nargs="*") + args = parser.parse_args() + + modifier = to_upper_bound if args.upper else to_lower_bound + + req_files = [filename for filename in args.filename if not filename.endswith("pyproject.toml")] + pyp_files = [filename for filename in args.filename if filename.endswith("pyproject.toml")] + if req_files: + with fileinput.input(files=req_files) as req_file: + for line in req_file: + if line.strip().startswith("#"): + # Shortcut comment only lines + print(line.strip()) + else: + req, comment = split_comment(line) + new_req = modifier(req) + print(new_req + comment) + for filename in pyp_files: + with open(filename, "rb") as fp: + pyproject = tomllib.load(fp) + for req in pyproject["project"]["dependencies"]: + new_req = modifier(req) + print(new_req) + optional_dependencies = pyproject["project"].get("optional-dependencies") + if optional_dependencies: + for opt in optional_dependencies.values(): + for req in opt: + new_req = modifier(req) + print(new_req) + + +if __name__ == "__main__": + main() diff --git a/.ci/scripts/check_cli_dependencies.py b/.ci/scripts/check_cli_dependencies.py new file mode 100755 index 0000000..0651e8f --- /dev/null +++ b/.ci/scripts/check_cli_dependencies.py @@ -0,0 +1,36 @@ +#!/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "packaging>=25.0,<25.1", +# ] +# /// + +import typing as t +from pathlib import Path + +import tomllib +from packaging.requirements import Requirement + +GLUE_DIR = "pulp-glue-npm" + + +def dependencies(path: Path) -> t.Iterator[Requirement]: + with (path / "pyproject.toml").open("rb") as fp: + pyproject = tomllib.load(fp) + + return (Requirement(r) for r in pyproject["project"]["dependencies"]) + + +if __name__ == "__main__": + base_path = Path(__file__).parent.parent.parent + glue_path = base_path / GLUE_DIR + + cli_dependency = next((r for r in dependencies(base_path) if r.name == "pulp-cli")) + glue_dependency = next((r for r in dependencies(glue_path) if r.name == "pulp-glue")) + + if cli_dependency.specifier != glue_dependency.specifier: + print("🪢 CLI and GLUE dependencies mismatch:") + print(" ", cli_dependency) + print(" ", glue_dependency) + exit(1) diff --git a/.ci/scripts/check_click_for_mypy.py b/.ci/scripts/check_click_for_mypy.py new file mode 100755 index 0000000..33ecf4c --- /dev/null +++ b/.ci/scripts/check_click_for_mypy.py @@ -0,0 +1,18 @@ +#!/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "packaging>=25.0,<25.1", +# ] +# /// + +from importlib import metadata + +from packaging.version import Version + +if __name__ == "__main__": + click_version = Version(metadata.version("click")) + if click_version < Version("8.1.1"): + print("🚧 Linting with mypy is currently only supported with click>=8.1.1. 🚧") + print("🔧 Please run `pip install click>=8.1.1` first. 🔨") + exit(1) diff --git a/.ci/scripts/collect_changes.py b/.ci/scripts/collect_changes.py new file mode 100755 index 0000000..499265c --- /dev/null +++ b/.ci/scripts/collect_changes.py @@ -0,0 +1,115 @@ +#!/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "gitpython>=3.1.46,<3.2.0", +# "packaging>=25.0,<25.1", +# ] +# /// + +import itertools +import os +import re + +import tomllib +from git import GitCommandError, Repo +from packaging.version import parse as parse_version + +# Read Towncrier settings +with open("pyproject.toml", "rb") as fp: + tc_settings = tomllib.load(fp)["tool"]["towncrier"] + +CHANGELOG_FILE = tc_settings.get("filename", "NEWS.rst") +START_STRING = tc_settings.get( + "start_string", + ( + "\n" + if CHANGELOG_FILE.endswith(".md") + else ".. towncrier release notes start\n" + ), +) +TITLE_FORMAT = tc_settings.get("title_format", "{name} {version} ({project_date})") + + +# Build a regex to find the header of a changelog section. +# It must have a single capture group to single out the version. +# see help(re.split) for more info. +NAME_REGEX = r".*" +VERSION_REGEX = r"[0-9]+\.[0-9]+\.[0-9][0-9ab]*" +VERSION_CAPTURE_REGEX = rf"({VERSION_REGEX})" +DATE_REGEX = r"[0-9]{4}-[0-9]{2}-[0-9]{2}" +TITLE_REGEX = ( + "(" + + re.escape( + TITLE_FORMAT.format(name="NAME_REGEX", version="VERSION_REGEX", project_date="DATE_REGEX") + ) + .replace("NAME_REGEX", NAME_REGEX) + .replace("VERSION_REGEX", VERSION_CAPTURE_REGEX, 1) + .replace("VERSION_REGEX", VERSION_REGEX) + .replace("DATE_REGEX", DATE_REGEX) + + ")" +) + + +def get_changelog(repo, branch): + branch_tc_settings = tomllib.loads(repo.git.show(f"{branch}:pyproject.toml"))["tool"][ + "towncrier" + ] + branch_changelog_file = branch_tc_settings.get("filename", "NEWS.rst") + return repo.git.show(f"{branch}:{branch_changelog_file}") + "\n" + + +def _tokenize_changes(splits): + assert len(splits) % 3 == 0 + for i in range(len(splits) // 3): + title = splits[3 * i] + version = parse_version(splits[3 * i + 1]) + yield [version, title + splits[3 * i + 2]] + + +def split_changelog(changelog): + preamble, rest = changelog.split(START_STRING, maxsplit=1) + split_rest = re.split(TITLE_REGEX, rest) + return preamble + START_STRING + split_rest[0], list(_tokenize_changes(split_rest[1:])) + + +def main(): + repo = Repo(os.getcwd()) + remote = repo.remotes[0] + branches = [ref for ref in remote.refs if re.match(r"^([0-9]+)\.([0-9]+)$", ref.remote_head)] + branches.sort(key=lambda ref: parse_version(ref.remote_head), reverse=True) + branches = [ref.name for ref in branches] + + with open(CHANGELOG_FILE, "r") as f: + main_changelog = f.read() + preamble, main_changes = split_changelog(main_changelog) + old_length = len(main_changes) + + for branch in branches: + print(f"Looking at branch {branch}") + try: + changelog = get_changelog(repo, branch) + except GitCommandError: + print("No changelog found on this branch.") + continue + dummy, changes = split_changelog(changelog) + new_changes = sorted(main_changes + changes, key=lambda x: x[0], reverse=True) + # Now remove duplicates (retain the first one) + main_changes = [new_changes[0]] + for left, right in itertools.pairwise(new_changes): + if left[0] != right[0]: + main_changes.append(right) + + new_length = len(main_changes) + if old_length < new_length: + print(f"{new_length - old_length} new versions have been added.") + with open(CHANGELOG_FILE, "w") as fp: + fp.write(preamble) + for change in main_changes: + fp.write(change[1]) + + repo.git.commit("-m", "Update Changelog", CHANGELOG_FILE) + + +if __name__ == "__main__": + main() diff --git a/.ci/scripts/create_release_branch.sh b/.ci/scripts/create_release_branch.sh new file mode 100755 index 0000000..2e55c80 --- /dev/null +++ b/.ci/scripts/create_release_branch.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -eu -o pipefail + +BRANCH="$(git branch --show-current)" + +if ! [[ "${BRANCH}" = "main" ]] +then + echo ERROR: This is not the main branch! + exit 1 +fi + +NEW_BRANCH="$(bump-my-version show new_version --increment release | sed -Ene 's/^([[:digit:]]+\.[[:digit:]]+)\.[[:digit:]]+$/\1/p')" + +if [[ -z "${NEW_BRANCH}" ]] +then + echo ERROR: Could not parse new version. + exit 1 +fi + +git branch "${NEW_BRANCH}" + +# Clean changelog snippets. +find CHANGES/ \( -name "*.feature" -o -name "*.bugfix" -o -name "*.removal" -o -name "*.doc" -o -name "*.translation" -o -name "*.devel" -o -name "*.misc" \) -exec git rm -f \{\} + + +bump-my-version bump minor --commit --message $'Bump version to {new_version}' --allow-dirty + +git push origin "${NEW_BRANCH}" + +if [ "${GITHUB_ENV:-}" ] +then + echo "NEW_BRANCH=${NEW_BRANCH}" >> "${GITHUB_ENV}" +fi diff --git a/.ci/scripts/pr_labels.py b/.ci/scripts/pr_labels.py new file mode 100755 index 0000000..49eb4ad --- /dev/null +++ b/.ci/scripts/pr_labels.py @@ -0,0 +1,63 @@ +#!/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "gitpython>=3.1.46,<3.2.0", +# ] +# /// + +# This script is running with elevated privileges from the main branch against pull requests. + +import re +import sys +from pathlib import Path + +import tomllib +from git import Repo + + +def main(): + assert len(sys.argv) == 3 + + with open("pyproject.toml", "rb") as fp: + PYPROJECT_TOML = tomllib.load(fp) + BLOCKING_REGEX = re.compile(r"DRAFT|WIP|NO\s*MERGE|DO\s*NOT\s*MERGE|EXPERIMENT") + ISSUE_REGEX = re.compile(r"(?:fixes|closes)[\s:]+#(\d+)") + CHERRY_PICK_REGEX = re.compile(r"^\s*\(cherry picked from commit [0-9a-f]*\)\s*$") + CHANGELOG_EXTS = [ + f".{item['directory']}" for item in PYPROJECT_TOML["tool"]["towncrier"]["type"] + ] + + repo = Repo(".") + + base_commit = repo.commit(sys.argv[1]) + head_commit = repo.commit(sys.argv[2]) + + pr_commits = list(repo.iter_commits(f"{base_commit}..{head_commit}")) + + labels = { + "multi-commit": len(pr_commits) > 1, + "cherry-pick": False, + "no-issue": False, + "no-changelog": False, + "wip": False, + } + for commit in pr_commits: + labels["wip"] |= BLOCKING_REGEX.search(commit.summary) is not None + no_issue = ISSUE_REGEX.search(commit.message, re.IGNORECASE) is None + labels["no-issue"] |= no_issue + cherry_pick = CHERRY_PICK_REGEX.search(commit.message) is not None + labels["cherry-pick"] |= cherry_pick + changelog_snippets = [ + k + for k in commit.stats.files + if k.startswith("CHANGES/") and Path(k).suffix in CHANGELOG_EXTS + ] + labels["no-changelog"] |= not changelog_snippets + + print("ADD_LABELS=" + ",".join((k for k, v in labels.items() if v))) + print("REMOVE_LABELS=" + ",".join((k for k, v in labels.items() if not v))) + + +if __name__ == "__main__": + main() diff --git a/.ci/scripts/release.sh b/.ci/scripts/release.sh new file mode 100755 index 0000000..a9c3644 --- /dev/null +++ b/.ci/scripts/release.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -eu -o pipefail + +BRANCH=$(git branch --show-current) + +if ! [[ "${BRANCH}" =~ ^[0-9]+\.[0-9]+$ ]] +then + echo ERROR: This is not a release branch! + exit 1 +fi + +NEW_VERSION="$(bump-my-version show new_version --increment release)" +echo "Release ${NEW_VERSION}" + +if ! [[ "${NEW_VERSION}" == "${BRANCH}"* ]] +then + echo ERROR: Version does not match release branch + exit 1 +fi + +towncrier build --yes --version "${NEW_VERSION}" +bump-my-version bump release --commit --message "Release {new_version}" --tag --tag-name "{new_version}" --tag-message "Release {new_version}" --allow-dirty +bump-my-version bump patch --commit + +git push origin "${BRANCH}" "${NEW_VERSION}" diff --git a/.ci/scripts/validate_commit_message.py b/.ci/scripts/validate_commit_message.py new file mode 100644 index 0000000..a5a3712 --- /dev/null +++ b/.ci/scripts/validate_commit_message.py @@ -0,0 +1,75 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "pygithub>=2.8.1,<3.0.0", +# ] +# /// + +import os +import re +import subprocess +import sys +from pathlib import Path + +import tomllib +from github import Github + +with open("pyproject.toml", "rb") as fp: + PYPROJECT_TOML = tomllib.load(fp) +KEYWORDS = ["fixes", "closes"] +BLOCKING_REGEX = [ + "DRAFT", + "WIP", + "NOMERGE", + r"DO\s*NOT\s*MERGE", + "EXPERIMENT", +] +CHANGELOG_EXTS = [f".{item['directory']}" for item in PYPROJECT_TOML["tool"]["towncrier"]["type"]] +NOISSUE_MARKER = "[noissue]" + +sha = sys.argv[1] +message = subprocess.check_output(["git", "log", "--format=%B", "-n 1", sha]).decode("utf-8") + +if NOISSUE_MARKER in message: + sys.exit("Do not add '[noissue]' in the commit message.") + +if any((re.match(pattern, message) for pattern in BLOCKING_REGEX)): + sys.exit("This PR is not ready for consumption.") + +g = Github(os.environ.get("GITHUB_TOKEN")) +repo = g.get_repo("pulp/pulp-cli-npm") + + +def check_status(issue): + gi = repo.get_issue(int(issue)) + if gi.pull_request: + sys.exit(f"Error: issue #{issue} is a pull request.") + if gi.closed_at: + sys.exit(f"Error: issue #{issue} is closed.") + + +def check_changelog(issue): + matches = list(Path("CHANGES").rglob(f"{issue}.*")) + + if len(matches) < 1: + sys.exit(f"Could not find changelog entry in CHANGES/ for {issue}.") + for match in matches: + if match.suffix not in CHANGELOG_EXTS: + sys.exit(f"Invalid extension for changelog entry '{match}'.") + + +print("Checking commit message for {sha}.".format(sha=sha[0:7])) + +# validate the issue attached to the commit +issue_regex = r"(?:{keywords})[\s:]+#(\d+)".format(keywords=("|").join(KEYWORDS)) +issues = re.findall(issue_regex, message, re.IGNORECASE) +cherry_pick_regex = r"^\s*\(cherry picked from commit [0-9a-f]*\)\s*$" +cherry_pick = re.search(cherry_pick_regex, message, re.MULTILINE) + +if issues: + for issue in issues: + if not cherry_pick: + check_status(issue) + check_changelog(issue) + +print("Commit message for {sha} passed.".format(sha=sha[0:7])) diff --git a/.ci/settings/settings.py b/.ci/settings/settings.py new file mode 100644 index 0000000..362d83f --- /dev/null +++ b/.ci/settings/settings.py @@ -0,0 +1,59 @@ +import os + +ALLOWED_EXPORT_PATHS = ["/tmp"] +ANALYTICS = False +ALLOWED_CONTENT_CHECKSUMS = ["sha1", "sha256", "sha512"] +TASK_DIAGNOSTICS = ["memory"] + +if os.environ.get("PULP_HTTPS", "false").lower() == "true": + AUTHENTICATION_BACKENDS = "@merge django.contrib.auth.backends.RemoteUserBackend" + MIDDLEWARE = "@merge django.contrib.auth.middleware.RemoteUserMiddleware" + REST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSES = ( + "@merge pulpcore.app.authentication.PulpRemoteUserAuthentication" + ) + REMOTE_USER_ENVIRON_NAME = "HTTP_REMOTEUSER" + +if os.environ.get("PULP_OAUTH2", "false").lower() == "true": + assert os.environ.get("PULP_HTTPS", "false").lower() == "true" + + def PulpCliFakeOauth2Authentication(*args, **kwargs): + # We need to lazy load this. + # Otherwise views may be instanciated, before this configuration is merged. + + from django.contrib.auth import authenticate + from drf_spectacular.extensions import OpenApiAuthenticationExtension + from rest_framework.authentication import BaseAuthentication + + class _PulpCliFakeOauth2Authentication(BaseAuthentication): + def authenticate(self, request): + auth_header = request.META.get("HTTP_AUTHORIZATION") + if auth_header == "Bearer DEADBEEF": + return authenticate(request, remote_user="admin"), None + else: + return None + + def authenticate_header(self, request): + return 'Bearer realm="Pulp"' + + class PulpCliFakeOauth2AuthenticationScheme(OpenApiAuthenticationExtension): + target_class = _PulpCliFakeOauth2Authentication + name = "PulpCliFakeOauth2" + + def get_security_definition(self, auto_schema): + return { + "type": "oauth2", + "flows": { + "clientCredentials": { + "tokenUrl": "https://localhost:8080/oauth2token/", + "scopes": {"api.console": "grant_access_to_pulp"}, + }, + }, + } + + return _PulpCliFakeOauth2Authentication(*args, **kwargs) + + PULP_CLI_FAKE_OAUTH2_AUTHENTICATION = PulpCliFakeOauth2Authentication + + REST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSES = ( + "@merge pulpcore.app.settings.PULP_CLI_FAKE_OAUTH2_AUTHENTICATION" + ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..af562a7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +--- +name: "Build" + +on: + workflow_call: + +jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + - uses: "actions/cache@v5" + with: + path: "~/.cache/pip" + key: "${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/*constraints.lock', '**/setup.py', '**/pyproject.toml') }}" + restore-keys: | + ${{ runner.os }}-pip- + + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.14" + - name: "Install python dependencies" + run: | + pip install build setuptools wheel + - name: "Build wheels" + run: | + make build + - name: "Upload wheels" + uses: "actions/upload-artifact@v6" + with: + name: "pulp_cli_packages" + path: | + pulp-glue-npm/dist/ + dist/ + if-no-files-found: "error" + retention-days: 5 +... diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3c8da25 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,31 @@ +--- +name: "CodeQL" + +on: + push: + branches: + - "main" + workflow_call: + +jobs: + analyze: + name: "Analyze" + runs-on: "ubuntu-latest" + permissions: + actions: "read" + contents: "read" + security-events: "write" + + steps: + - name: "Checkout repository" + uses: "actions/checkout@v6" + - name: "Initialize CodeQL" + uses: "github/codeql-action/init@v4" + with: + languages: "python" + + - name: "Perform CodeQL Analysis" + uses: "github/codeql-action/analyze@v4" + with: + category: "/language:python" +... diff --git a/.github/workflows/collect_changes.yml b/.github/workflows/collect_changes.yml new file mode 100644 index 0000000..f5ead77 --- /dev/null +++ b/.github/workflows/collect_changes.yml @@ -0,0 +1,42 @@ +--- +name: "Collect changes" +on: + workflow_call: + workflow_dispatch: + +jobs: + collect-changes: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + with: + ref: "main" + fetch-depth: 0 + - uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Setup git" + run: | + git config user.name pulpbot + git config user.email pulp-infra@redhat.com + - name: "Collect changes" + run: | + pip install GitPython packaging + python3 .ci/scripts/collect_changes.py + - name: "Create Pull Request" + uses: "peter-evans/create-pull-request@v8" + id: "create_pr" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + title: "Update Changelog" + body: "" + branch: "update_changes" + delete-branch: true + - name: "Mark PR automerge" + run: | + gh pr merge --rebase --auto "${{ steps.create_pr.outputs.pull-request-number }}" + if: "steps.create_pr.outputs.pull-request-number" + env: + GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}" + continue-on-error: true +... diff --git a/.github/workflows/cookiecutter.yml b/.github/workflows/cookiecutter.yml new file mode 100644 index 0000000..c8dae4a --- /dev/null +++ b/.github/workflows/cookiecutter.yml @@ -0,0 +1,107 @@ +--- +name: "Update CI from cookiecutter" +on: + workflow_dispatch: + schedule: + - cron: "30 18 * * 0" + +defaults: + run: + working-directory: "pulp-cli-npm" + +jobs: + update-ci: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + with: + repository: "pulp/pulp-cli" + path: "pulp-cli" + - uses: "actions/checkout@v6" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + path: "pulp-cli-npm" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Setup git" + run: | + git config user.name pulpbot + git config user.email pulp-infra@redhat.com + - name: "Install python dependencies" + run: | + pip install cookiecutter tomlkit + - name: "Apply cookiecutter templates" + run: | + ../pulp-cli/cookiecutter/apply_templates.py + if [ "$(git status --porcelain)" ] + then + git add . + git commit -m "Update cookiecutter" + fi + - name: "Create Pull Request" + uses: "peter-evans/create-pull-request@v8" + id: "create_pr" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + title: "Update cookiecutter" + body: "" + branch: "update_cookiecutter" + delete-branch: true + path: "pulp-cli-npm" + - name: "Mark PR automerge" + run: | + gh pr merge --rebase --auto "${{ steps.create_pr.outputs.pull-request-number }}" + if: "steps.create_pr.outputs.pull-request-number" + env: + GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}" + continue-on-error: true + update-dependencies: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + with: + repository: "pulp/pulp-cli" + path: "pulp-cli" + - uses: "actions/checkout@v6" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + path: "pulp-cli-npm" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Setup git" + run: | + git config user.name pulpbot + git config user.email pulp-infra@redhat.com + - name: "Install python dependencies" + run: | + pip install packaging tomlkit + - name: "Apply cookiecutter templates" + run: | + ../pulp-cli/cookiecutter/update_pulp_cli.py + if [ "$(git status --porcelain)" ] + then + git add . + git commit -m "Update CLI and GLUE" + fi + - name: "Create Pull Request" + uses: "peter-evans/create-pull-request@v8" + id: "create_pr" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + title: "Update CLI and GLUE" + body: "" + branch: "update_cli" + delete-branch: true + path: "pulp-cli-npm" + - name: "Mark PR automerge" + run: | + gh pr merge --rebase --auto "${{ steps.create_pr.outputs.pull-request-number }}" + if: "steps.create_pr.outputs.pull-request-number" + env: + GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}" + continue-on-error: true +... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8b0b50b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +--- +name: "Lint" + +on: + workflow_call: + +jobs: + lint: + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + python: + - "3.11" + - "3.14" + steps: + - uses: "actions/checkout@v6" + - uses: "actions/cache@v5" + with: + path: "~/.cache/pip" + key: "${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/*constraints.lock', '**/setup.py', '**/pyproject.toml') }}" + restore-keys: | + ${{ runner.os }}-pip- + + - name: "Download wheels" + uses: "actions/download-artifact@v8" + with: + name: "pulp_cli_packages" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "${{ matrix.python }}" + - name: "Install python dependencies" + run: | + pip install dist/pulp_cli_npm-*.whl pulp-glue-npm/dist/pulp_glue_npm-*.whl -r lint_requirements.txt + - name: "Lint code" + run: | + make lint +... diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..cf39225 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,21 @@ +--- +name: "pulp-cli Nightly" + +on: + schedule: + - cron: "15 3 * * *" + workflow_dispatch: + +jobs: + build: + uses: "./.github/workflows/build.yml" + test: + needs: + - "build" + uses: "./.github/workflows/test.yml" + codeql: + uses: "./.github/workflows/codeql.yml" + collect_changes: + uses: "./.github/workflows/collect_changes.yml" + secrets: "inherit" +... diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..e201323 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,68 @@ +--- +name: "pulp-cli CI" + +on: + pull_request: + +concurrency: + group: "main-${{ github.ref_name }}-${{ github.workflow }}" + cancel-in-progress: true + +jobs: + build: + uses: "./.github/workflows/build.yml" + lint: + needs: + - "build" + uses: "./.github/workflows/lint.yml" + test: + needs: + - "lint" + uses: "./.github/workflows/test.yml" + codeql: + needs: + - "lint" + uses: "./.github/workflows/codeql.yml" + check-commits: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + with: + fetch-depth: 0 + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Install python dependencies" + run: | + pip install toml pygithub + - name: "Check commit message" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GITHUB_CONTEXT: "${{ github.event.pull_request.commits_url }}" + run: | + for SHA in $(curl -H "Authorization: token $GITHUB_TOKEN" "$GITHUB_CONTEXT" | jq -r '.[].sha') + do + python .ci/scripts/validate_commit_message.py "$SHA" + VALUE=$? + if [ "$VALUE" -gt 0 ]; then + exit "$VALUE" + fi + done + shell: "bash" + ready-to-ship: + # This is a dummy dependent task to have a single entry for the branch protection rules. + runs-on: "ubuntu-latest" + needs: + - "check-commits" + - "lint" + - "test" + - "codeql" + if: "always()" + steps: + - name: "Collect needed jobs results" + run: | + echo '${{toJson(needs)}}' | jq -r 'to_entries[]|select(.value.result!="success")|.key + ": " + .value.result' + echo '${{toJson(needs)}}' | jq -e 'to_entries|map(select(.value.result!="success"))|length == 0' + echo "CI says: Looks good!" +... diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml new file mode 100644 index 0000000..43f5c94 --- /dev/null +++ b/.github/workflows/pr_checks.yml @@ -0,0 +1,60 @@ +--- +name: "Pulp CLI PR static checks" +on: + pull_request_target: + types: ["opened", "synchronize", "reopened"] + +# This workflow runs with elevated permissions. +# Do not even think about running a single bit of code from the PR. +# Static analysis should be fine however. + +concurrency: + group: "${{ github.event.pull_request.number }}-${{ github.workflow }}" + cancel-in-progress: true + +jobs: + apply_labels: + runs-on: "ubuntu-latest" + name: "Label PR" + permissions: + pull-requests: "write" + steps: + - uses: "actions/checkout@v6" + with: + fetch-depth: 0 + - uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Determine PR labels" + run: | + pip install GitPython==3.1.42 + git fetch origin ${{ github.event.pull_request.head.sha }} + python .ci/scripts/pr_labels.py "origin/${{ github.base_ref }}" "${{ github.event.pull_request.head.sha }}" >> "$GITHUB_ENV" + - uses: "actions/github-script@v8" + name: "Apply PR Labels" + with: + script: | + const { ADD_LABELS, REMOVE_LABELS } = process.env; + + if (REMOVE_LABELS.length) { + for await (const labelName of REMOVE_LABELS.split(",")) { + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + }); + } catch(err) { + } + } + } + if (ADD_LABELS.length) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ADD_LABELS.split(","), + }); + } +... diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..98ab120 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,39 @@ +--- +name: "pulp-cli Publish" + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +jobs: + build: + uses: "./.github/workflows/build.yml" + publish-pypi: + name: "Publish to PyPI" + needs: "build" + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + - name: "Download wheels" + uses: "actions/download-artifact@v8" + with: + name: "pulp_cli_packages" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Install dependencies" + run: | + python -m pip install --upgrade pip + pip install twine + - name: "Build and publish" + env: + TWINE_USERNAME: "__token__" + TWINE_PASSWORD: "${{ secrets.PYPI_API_TOKEN }}" + run: | + cd pulp-glue-npm + twine upload dist/* + cd .. + twine upload dist/* +... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4cbd745 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +--- +name: "pulp-cli Release" + +on: + workflow_dispatch: + +jobs: + release: + name: "Release" + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Install dependencies" + run: | + python -m pip install --upgrade pip + pip install bump-my-version~=0.20.0 towncrier~=23.11.0 + - name: "Setup git" + run: | + git config user.name pulpbot + git config user.email pulp-infra@redhat.com + - name: "Release" + run: | + .ci/scripts/release.sh +... diff --git a/.github/workflows/release_branch.yml b/.github/workflows/release_branch.yml new file mode 100644 index 0000000..a75f0f3 --- /dev/null +++ b/.github/workflows/release_branch.yml @@ -0,0 +1,56 @@ +--- +name: "Create Release Branch" +on: + workflow_dispatch: + +jobs: + create-release-branch: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v6" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.x" + - name: "Setup git" + run: | + git config user.name pulpbot + git config user.email pulp-infra@redhat.com + - name: "Install python dependencies" + run: | + pip install bump-my-version~=0.20.0 + - name: "Create Release Branch" + run: | + .ci/scripts/create_release_branch.sh + - name: "Create Pull Request" + uses: "peter-evans/create-pull-request@v8" + id: "create_pr" + with: + token: "${{ secrets.RELEASE_TOKEN }}" + title: "Bump dev-version" + body: "" + branch: "bump_version" + delete-branch: true + - name: "Mark PR automerge" + run: | + gh pr merge --rebase --auto "${{ steps.create_pr.outputs.pull-request-number }}" + if: "steps.create_pr.outputs.pull-request-number" + env: + GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}" + continue-on-error: true + - name: "Add Backport Label for new Branch" + uses: "actions/github-script@v8" + with: + script: | + const { NEW_BRANCH } = process.env; + const labelName = "backport-" + NEW_BRANCH; + + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + color: "C8780A", + }); +... diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2678df..2e8813e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,20 @@ +--- +name: "Test" +on: + workflow_call: + +env: + COLORTERM: "yes" + TERM: "xterm-256color" + PYTEST_ADDOPTS: "--color=yes" + CONTAINER_RUNTIME: "docker" jobs: test: + runs-on: "ubuntu-24.04" strategy: + fail-fast: false matrix: include: - image_tag: "nightly" @@ -11,3 +23,48 @@ jobs: - image_tag: "latest" lower_bounds: true python: "3.8" + steps: + - uses: "actions/checkout@v6" + - uses: "actions/cache@v5" + with: + path: "~/.cache/pip" + key: "${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/*constraints.lock', '**/setup.py', '**/pyproject.toml') }}" + restore-keys: | + ${{ runner.os }}-pip- + + - name: "Download wheels" + uses: "actions/download-artifact@v8" + with: + name: "pulp_cli_packages" + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "${{ matrix.python }}" + allow-prereleases: true + - name: "Install Python Test Dependencies" + run: | + if [ "${{matrix.lower_bounds}}" ] + then + pip install dist/pulp_cli_npm-*.whl pulp-glue-npm/dist/pulp_glue_npm-*.whl -r test_requirements.txt -c lower_bounds_constraints.lock + elif [ "${{matrix.upper_bounds}}" ] + then + .ci/scripts/calc_constraints.py pyproject.toml pulp-glue-npm/pyproject.toml --upper > upper_bounds_constraints.lock + pip install dist/pulp_cli_npm-*.whl pulp-glue-npm/dist/pulp_glue_npm-*.whl -r test_requirements.txt -c upper_bounds_constraints.lock + else + pip install dist/pulp_cli_npm-*.whl pulp-glue-npm/dist/pulp_glue_npm-*.whl -r test_requirements.txt + fi + - name: "Run tests" + env: + CONTAINER_RUNTIME: "${{ matrix.container_runtime }}" + IMAGE_TAG: "${{ matrix.image_tag }}" + FROM_TAG: "${{ matrix.from_tag }}" + CONTAINER_FILE: "${{ matrix.container_file }}" + PULP_HTTPS: "${{ matrix.pulp_https }}" + PULP_OAUTH2: "${{ matrix.pulp_oauth2 }}" + PULP_API_ROOT: "${{ matrix.pulp_api_root }}" + PULP_DOMAIN_ENABLED: "${{ matrix.pulp_domain_enabled }}" + PULP_ENABLED_PLUGINS: "${{ matrix.pulp_enabled_plugins }}" + OAS_VERSION: "${{ matrix.oas_version }}" + run: | + .ci/run_container.sh make test +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed761f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.egg-info +__pycache__/ +build/ +tests/cli.toml +GPG-PRIVATE-KEY-fixture-signing +site/ +dist/ +*.po~ diff --git a/CHANGES/.TEMPLATE.md b/CHANGES/.TEMPLATE.md new file mode 100644 index 0000000..1b9b356 --- /dev/null +++ b/CHANGES/.TEMPLATE.md @@ -0,0 +1,42 @@ + +{# TOWNCRIER TEMPLATE #} +{% for section, _ in sections.items() %} +{%- set section_slug = "-" + section|replace(" ", "-")|replace("_", "-")|lower %} +{% if section %}### {{section}} {: #{{versiondata.version}}{{section_slug}} } + +{% else %} +{%- set section_slug = "" %} +{% endif %} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section]%} +#### {{ definitions[category]['name'] }} {: #{{versiondata.version}}{{section_slug}}-{{category}} } + +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} +- {{ text }} +{% if values %} + {{ values|join(',\n ') }} +{% endif %} +{% endfor %} + +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. + +{% else %} +{% endif %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} +--- + + diff --git a/CHANGES/pulp-glue-npm/.gitkeep b/CHANGES/pulp-glue-npm/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c125fe0 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ + +GLUE_PLUGINS=$(notdir $(wildcard pulp-glue-npm/src/pulp_glue/*)) +CLI_PLUGINS=$(notdir $(wildcard src/pulpcore/cli/*)) + +.PHONY: info +info: + @echo Pulp glue + @echo plugins: $(GLUE_PLUGINS) + @echo Pulp CLI + @echo plugins: $(CLI_PLUGINS) + +.PHONY: build +build: + cd pulp-glue-npm; pyproject-build -n + pyproject-build -n + +.PHONY: format +format: + ruff format + ruff check --fix + +.PHONY: lint +lint: + find tests .ci -name '*.sh' -print0 | xargs -0 shellcheck -x + ruff format --check --diff + ruff check --diff + .ci/scripts/check_cli_dependencies.py + .ci/scripts/check_click_for_mypy.py + mypy + cd pulp-glue-npm; mypy + @echo "🙊 Code 🙈 LGTM 🙉 !" + +tests/cli.toml: + cp $@.example $@ + @echo "In order to configure the tests to talk to your test server, you might need to edit $@ ." + +.PHONY: test +test: | tests/cli.toml + python3 -m pytest -v tests pulp-glue-npm/tests + +.PHONY: livetest +livetest: | tests/cli.toml + python3 -m pytest -v tests pulp-glue-npm/tests -m live + +.PHONY: paralleltest +paralleltest: | tests/cli.toml + python3 -m pytest -v tests pulp-glue-npm/tests -m live -n 8 + +.PHONY: unittest +unittest: + python3 -m pytest -v tests pulp-glue-npm/tests -m "not live" + +.PHONY: unittest_glue +unittest_glue: + python3 -m pytest -v pulp-glue-npm/tests -m "not live" diff --git a/lint_requirements.txt b/lint_requirements.txt new file mode 100644 index 0000000..1246a40 --- /dev/null +++ b/lint_requirements.txt @@ -0,0 +1,14 @@ +# Lint requirements +ruff~=0.15.1 +mypy~=1.19.1 +shellcheck-py~=0.11.0.1 + +# Type annotation stubs +types-pygments +types-PyYAML +types-requests +types-setuptools +types-toml + +# Install the actual bits for mypy +-r test_requirements.txt diff --git a/pulp-glue-npm/pyproject.toml b/pulp-glue-npm/pyproject.toml index 61a7224..680e9a5 100644 --- a/pulp-glue-npm/pyproject.toml +++ b/pulp-glue-npm/pyproject.toml @@ -30,3 +30,39 @@ repository = "https://github.com/pulp/pulp-cli-npm" changelog = "https://github.com/pulp/pulp-cli-npm/blob/main/CHANGES.md" [tool.mypy] +# This section is managed by the cookiecutter templates. +strict = true +warn_unused_ignores = false +show_error_codes = true +files = "src/**/*.py, tests/**/*.py" +mypy_path = ["src"] +namespace_packages = true +explicit_package_bases = true + +[[tool.mypy.overrides]] +# This section is managed by the cookiecutter templates. +module = [ + "schema.*", +] +ignore_missing_imports = true + + +[tool.setuptools.packages.find] +# This section is managed by the cookiecutter templates. +where = ["src"] +include = ["pulp_glue.*"] +namespaces = true + +[tool.setuptools.package-data] +# This section is managed by the cookiecutter templates. +"*" = ["py.typed"] + + +[tool.ruff] +# This section is managed by the cookiecutter templates. +line-length = 100 + +[tool.ruff.lint] +# This section is managed by the cookiecutter templates. +extend-select = ["I"] + diff --git a/pyproject.toml b/pyproject.toml index 29d35e6..bfe2485 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,11 +34,18 @@ changelog = "https://github.com/pulp/pulp-cli-npm/blob/main/CHANGES.md" npm = "pulpcore.cli.npm" [tool.pulp_cli_template] +# This section is co-managed by the cookiecutter templates. +# Changes to existing keys should be preserved. app_label = "npm" +src_layout = true +repository = "https://github.com/pulp/pulp-cli-npm" glue = true docs = false -src_layout = true translations = false +main_package = "npm" +binary_dependencies = "" +unittests = false +paralleltests = false [tool.pytest.ini_options] markers = [ @@ -46,3 +53,150 @@ markers = [ "help_page: tests that render help pages", "pulp_npm: pulp_npm tests", ] + +[tool.setuptools.packages.find] +# This section is managed by the cookiecutter templates. +where = ["src"] +include = ["pulpcore.cli.*"] +namespaces = true + +[tool.setuptools.package-data] +# This section is managed by the cookiecutter templates. +"*" = ["py.typed"] + + +[tool.bumpversion] +# This section is managed by the cookiecutter templates. +current_version = "0.0.1.dev0" +commit = false +tag = false +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.(?P[a-z]+))?" +serialize = [ + "{major}.{minor}.{patch}.{release}", + "{major}.{minor}.{patch}", +] + +[tool.bumpversion.parts.release] +# This section is managed by the cookiecutter templates. +optional_value = "prod" +values = [ + "dev", + "prod", +] + +[[tool.bumpversion.files]] +# This section is managed by the cookiecutter templates. +filename = "./pulp-glue-npm/src/pulp_glue/npm/__init__.py" +search = "__version__ = \"{current_version}\"" +replace = "__version__ = \"{new_version}\"" + +[[tool.bumpversion.files]] +# This section is managed by the cookiecutter templates. +filename = "./src/pulpcore/cli/npm/__init__.py" +search = "__version__ = \"{current_version}\"" +replace = "__version__ = \"{new_version}\"" + +[[tool.bumpversion.files]] +# This section is managed by the cookiecutter templates. +filename = "./pulp-glue-npm/pyproject.toml" +search = "version = \"{current_version}\"" +replace = "version = \"{new_version}\"" + +[[tool.bumpversion.files]] +# This section is managed by the cookiecutter templates. +filename = "./pyproject.toml" +search = "version = \"{current_version}\"" +replace = "version = \"{new_version}\"" + +[[tool.bumpversion.files]] +# This section is managed by the cookiecutter templates. +filename = "./pyproject.toml" +search = "\"pulp-glue-npm=={current_version}\"" +replace = "\"pulp-glue-npm=={new_version}\"" + + +[tool.towncrier] +# This section is managed by the cookiecutter templates. +filename = "CHANGES.md" +directory = "CHANGES/" +title_format = "## {version} ({project_date}) {{: #{version} }}" +template = "CHANGES/.TEMPLATE.md" +issue_format = "[#{issue}](https://github.com/pulp/pulp-cli-npm/issues/{issue})" +start_string = "[//]: # (towncrier release notes start)\n" +underlines = ["", "", ""] + +[[tool.towncrier.section]] +# This section is managed by the cookiecutter templates. +path = "" +name = "" + +[[tool.towncrier.section]] +# This section is managed by the cookiecutter templates. +path = "pulp-glue-npm" +name = "Pulp-npm GLUE" + +[[tool.towncrier.type]] +# This section is managed by the cookiecutter templates. +directory = "feature" +name = "Features" +showcontent = true + +[[tool.towncrier.type]] +# This section is managed by the cookiecutter templates. +directory = "bugfix" +name = "Bugfixes" +showcontent = true + +[[tool.towncrier.type]] +# This section is managed by the cookiecutter templates. +directory = "removal" +name = "Deprecations and Removals" +showcontent = true + +[[tool.towncrier.type]] +# This section is managed by the cookiecutter templates. +directory = "devel" +name = "Developer Notes" +showcontent = true + +[[tool.towncrier.type]] +# This section is managed by the cookiecutter templates. +directory = "misc" +name = "Misc" +showcontent = true + + +[tool.ruff] +# This section is managed by the cookiecutter templates. +line-length = 100 +extend-exclude = ["cookiecutter"] + +[tool.ruff.lint] +# This section is managed by the cookiecutter templates. +extend-select = ["I"] + +[tool.ruff.lint.isort] +# This section is managed by the cookiecutter templates. +sections = { second-party = ["pulp_glue"] } +section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "local-folder"] + + +[tool.mypy] +# This section is managed by the cookiecutter templates. +strict = true +warn_unused_ignores = false +show_error_codes = true +files = "src/**/*.py, tests/*.py" +mypy_path = ["src", "pulp-glue-npm/src"] +namespace_packages = true +explicit_package_bases = true + +[[tool.mypy.overrides]] +# This section is managed by the cookiecutter templates. +module = [ + "click_shell.*", + "gnupg.*", + "IPython.*", + "schema.*", +] +ignore_missing_imports = true diff --git a/releasing.md b/releasing.md new file mode 100644 index 0000000..20d185a --- /dev/null +++ b/releasing.md @@ -0,0 +1,20 @@ +# Releasing (for internal use) + +## Using Workflows + +### Create a new Y-Release Branch + + 1. Trigger the "Create Release Branch" workflow on the "main" branch. + 1. Watch for the "Bump Version" PR, verify that it deletes all the changes snippets present on the new release branch, approve and merge it. + +### Release from a Release Branch + + 1. Trigger the "pulp-cli Release" workflow on the corresponding release branch. + 1. Lean back and see the magic happen. + 1. Wait for the "pulp-cli Publish" workflow to succeed. + 1. Verify that a new version appeared on PyPI. + 1. Verify that the docs have been updated. + 1. [only Y-releases] Announce the release at https://discourse.pulpproject.org/c/announcements/6. + 1. Look for the "Update Changelog" PR, approve and merge it. + +If some thing goes wrong look at `.ci/scripts/create_release_branch.sh` and `.ci/scripts/release.sh` and follow the intentions encoded there. diff --git a/tests/scripts/config.source b/tests/scripts/config.source new file mode 100644 index 0000000..cfa0a62 --- /dev/null +++ b/tests/scripts/config.source @@ -0,0 +1,107 @@ +# Library for test helper functions + +: "${PULP_API_ROOT:="/pulp/"}" + +# open fd 3 as a copy of stderr +exec 3<&2 + +# Expects the command to succeed +# Supresses all output, which is redirected to $OUTPUT and $ERROUTPUT +# Reports verbosely on failure +expect_succ () { + if { + "$@" + } 1>log.out 2>log.err + then + echo "SUCCESS [$@]" >&3 + OUTPUT="$(cat log.out)" + ERROUTPUT="$(cat log.err)" + else + echo "FAILURE [$@]" >&3 + echo "=== STDOUT ===" >&3 + cat log.out >&3 + echo "=== STDERR ===" >&3 + cat log.err >&3 + echo "==============" >&3 + false + fi +} + +# Expects the command to fail +# Supresses all output, which is redirected to $OUTPUT and $ERROUTPUT +# Reports verbosely on failure +expect_fail () { + if { + "$@" + } 1>log.out 2>log.err + then + echo "FAILURE [! $@]" >&3 + echo "=== STDOUT ===" >&3 + cat log.out >&3 + echo "=== STDERR ===" >&3 + cat log.err >&3 + false + else + echo "SUCCESS [! $@]" >&3 + OUTPUT="$(cat log.out)" + ERROUTPUT="$(cat log.err)" + fi +} + +# Expects the command to report access denied +# Supresses all output, which is redirected to $OUTPUT and $ERROUTPUT +# Reports verbosely on failure +expect_deny () { + if { + "$@" + } 1>log.out 2>log.err + # TODO check for access denied message + then + echo "FAILURE [! $@]" >&3 + echo "=== STDOUT ===" >&3 + cat log.out >&3 + echo "=== STDERR ===" >&3 + cat log.err >&3 + false + else + if grep -q "Operation .* not authorized." log.err + then + echo "SUCCESS [! $@]" >&3 + OUTPUT="$(cat log.out)" + ERROUTPUT="$(cat log.err)" + else + echo "FAILURE [! $@]" >&3 + echo "=== STDOUT ===" >&3 + cat log.out >&3 + echo "=== STDERR ===" >&3 + cat log.err >&3 + false + fi + fi +} + +# Expects the provided test to pass +# Supresses all output, which is redirected to ASSERTION_OUTPUT and +# ASSERTION_ERROUTPUT so as not to overwrite the latest OUTPUT and ERROUTPUT. +# That way we can test several assertions in a row. +# Reports verbosely on failure. +assert () { + if { + [[ "$@" ]] + } 1>log.out 2>log.err + then + echo "SUCCESS [$@]" >&3 + ASSERTION_OUTPUT="$(cat log.out)" + ASSERTION_ERROUTPUT="$(cat log.err)" + else + echo "FAILURE [$@]" >&3 + echo "=== STDOUT ===" >&3 + cat log.out >&3 + echo "=== STDERR ===" >&3 + cat log.err >&3 + echo "==============" >&3 + false + fi +} + +set -eu From e0e30fe4eb416e6809dc2f7de8f0139d308735fd Mon Sep 17 00:00:00 2001 From: Yasen Date: Thu, 19 Mar 2026 16:09:47 +0100 Subject: [PATCH 2/4] lint fixes --- .../npm/__pycache__/context.cpython-314.pyc | Bin 3036 -> 3004 bytes pulp-glue-npm/src/pulp_glue/npm/context.py | 1 - src/pulpcore/cli/npm/__init__.py | 2 +- .../npm/__pycache__/__init__.cpython-314.pyc | Bin 1718 -> 1718 bytes .../npm/__pycache__/content.cpython-314.pyc | Bin 3434 -> 3428 bytes .../__pycache__/distribution.cpython-314.pyc | Bin 2975 -> 2975 bytes .../npm/__pycache__/remote.cpython-314.pyc | Bin 2574 -> 2574 bytes .../__pycache__/repository.cpython-314.pyc | Bin 4764 -> 4764 bytes src/pulpcore/cli/npm/content.py | 13 ++++----- src/pulpcore/cli/npm/distribution.py | 17 +++++------- src/pulpcore/cli/npm/remote.py | 7 +++-- src/pulpcore/cli/npm/repository.py | 25 +++++++++--------- tests/test_help_pages.py | 3 ++- 13 files changed, 30 insertions(+), 38 deletions(-) diff --git a/pulp-glue-npm/src/pulp_glue/npm/__pycache__/context.cpython-314.pyc b/pulp-glue-npm/src/pulp_glue/npm/__pycache__/context.cpython-314.pyc index b2b8d5a056bc07ca62bbb9cb2283a98e13d4640b..e964e5e6be4e6bba455245a814060efa2a5761c0 100644 GIT binary patch delta 853 zcmaKr&ubGw6vt85OS^2sx?RyuL;HGX%1OmoV5(>vHAD-( z*c&#vwIOg8c?T{#uceRisY0%up$e1)29py+6fy}_{G z-?-Mlx%uxKSix*W4;n)GbXd3@#Ao2GScM}|HmI4;3{{*i^IzW`JCeg6Oe delta 873 zcmaKr&uSA<6vpq(Oy-cirY($Bq0X}6&UYM$IDPGm`I=z*Rmu3jWAt5NJFB#61OJ7>c0KN*;5QY4O) zA&%@yH7TqGaiK;*Ra|+XCWNYjs=3jDYJu7yjt(+&)q$K8%`s37R~e`&$1IU*{0rt( z)xK5l1YYoLar>dy@dB^g>3#o1t@r!1O{vUBrCFNhtI};|K)a8y_#5e#GAWk)r!+-# z{H)xiCVwU0O(s!JA<_W*b)2iwD;EnW$cQ4MguuElT*~-W`HdXkj4p=u6=7cn`xc*4 zMj&ENS-3P5;h+15+Xd7RW&Tc?XCeldTfJnOTWW$%^Md-)HL-2Tud4eaC}$9OU49mk zM-&kMKgVTkK}>U9n+%h2DUzF7g3j=J+FLk^p>GXOt9lBimERRnqv$h+7)KNk=+jTE z)t%PTvfW--wpZ%wE8%`7u&!ZUw^y6Z`5Vo8>*QhAup2Q0?~Vs}P7ch}-pzy4**!ka z+J=}IcG$3~7i>1->-;0jQO5hMvLQBSKsCHJF>P^hVf^>a_O+{zyAPf`+P>yrhhN~I cVdp8}NTQS;M>HBaCV;PqZz;YO+mtc<7dgR=B>(^b diff --git a/pulp-glue-npm/src/pulp_glue/npm/context.py b/pulp-glue-npm/src/pulp_glue/npm/context.py index 61e160b..4b33383 100644 --- a/pulp-glue-npm/src/pulp_glue/npm/context.py +++ b/pulp-glue-npm/src/pulp_glue/npm/context.py @@ -1,5 +1,4 @@ from pulp_glue.common.context import ( - EntityDefinition, PluginRequirement, PulpContentContext, PulpDistributionContext, diff --git a/src/pulpcore/cli/npm/__init__.py b/src/pulpcore/cli/npm/__init__.py index 161d98f..baf8dcf 100644 --- a/src/pulpcore/cli/npm/__init__.py +++ b/src/pulpcore/cli/npm/__init__.py @@ -1,10 +1,10 @@ import typing as t import click +from pulp_cli.generic import pulp_group from pulp_glue.common.i18n import get_translation -from pulp_cli.generic import pulp_group from pulpcore.cli.npm.content import content from pulpcore.cli.npm.distribution import distribution from pulpcore.cli.npm.remote import remote diff --git a/src/pulpcore/cli/npm/__pycache__/__init__.cpython-314.pyc b/src/pulpcore/cli/npm/__pycache__/__init__.cpython-314.pyc index b9974f7a231accb45d21f65cbc2e6b133d0a8d1a..440342d22ec93a6b9def2054bf730b5dcc1a4eaa 100644 GIT binary patch delta 105 zcmdnSyN#Dyn~#@^0SH(H_e|ujVq}}xrp?GQag(haS3zk`L40~qerbUw<1PO5)ROp; zqQt!7oWzpM{JhP)jAkst0$?@CIhlItsd=eInaM@WlkHihCl|7QW53Iw{*=M+>Et{% F4**_mA*uiX delta 110 zcmdnSyN#Dyn~#@^0SF5IY@f(o#mF+TO`DNz;wD=;{`Az6_>!W;yyBe1lFa-(O~zYX z1*JI!@##hRr3IUL8O>NEL?Hq>rKx(!`MJ6Id3u?K7I{U?lkHi>CKs}PV}HtE_>@8Y J?&Lf+4*-e$B#!_9 diff --git a/src/pulpcore/cli/npm/__pycache__/content.cpython-314.pyc b/src/pulpcore/cli/npm/__pycache__/content.cpython-314.pyc index 023a62e4c3957d6d27c86bc5b56a5d158f113cc0..2b7431c0c3166153f1bf5e312d74eef03656814c 100644 GIT binary patch delta 490 zcmXAlOK2295QeMf(KFN2J+qUHAUo=^x^W>m~Ne&^%N|3O2z?_Un z!3S%B=iKzBu;4{+dJqH=f+#)+VoshWpsc50PwaxP{;EUOU;V4TU6)glTt>k7PAurJ z|HW+H4CdImYQ;&K;jMV%?Ar?;Kp3X3N;6Z2L_x^9@y?HZl4vSxlbb(Ox~?sc%(1?hBt&K{*qQ2O{912zewTo)bBq7kOR~ zg%ZTh@X>sfFGJ0)IcmBwS;p8JzFx-M@F>Pe-omTM|t0=EoD;% zHW>#@{&M$KpQgR;%A! zYHYXL+Z|;(qB#&fQ{PtKd;i#fG0E!1PS5&huhVV+O}2HiqXWWea-)?jZ8ba1Uefr} zhO7=CFr(;c>1$98rXWvvuNDUgkeZ120dk+g{{q)O!Ri6jCmDjq1QdR1jAtP@ISTyq RsJ`nGJ>{x)^ybwezX62Zkoo`s diff --git a/src/pulpcore/cli/npm/__pycache__/distribution.cpython-314.pyc b/src/pulpcore/cli/npm/__pycache__/distribution.cpython-314.pyc index 542e66de736474f2d03691cdcd65ab989e5af63c..c6bc23f90aeb80020280844e6a04fbb14d582df8 100644 GIT binary patch delta 462 zcmZXOu}i~H5XRrVm!vT^t+h=X5vs+CsE|@ca2FvWb|?xiPmx+`p{b=lSJBlioZu+9 z2@ZmThz?GIn}dt`2Z&olq>jF1^$vdb-Fp@sg>ZhLX zMe_0>D$jgNZs2N6Ko*~36*D~DD@6A}7K{4g=s;(`)!D4InogtMu6LWYE|Ydw386~h zjlMkeW<^_#)_&co?RwsB+i8~P+g%Hf^kLNdKFSwBNWcK#oy2=I4YWu8<7bM%Oph|4 gUk2ulhv1CtkU8vpY5QcYlZ!ecKm#Deq4ug@zh^XMjLd05xlVFPLR}mErLgEjw*sIiM)slTj?=j;2n(f{sIvRyrom%V4iwlVpY+_1G)o!(S)be zMya=vRoYDiFiIxukLdd<-e{F}gDpeb;Fkb8Uh5lk^UB#PD;$z8(V^6d@ZwiO7S(w2 zbohAquP>H+^_9mvT*n|LLBso4)$*bXh2Dj52#dxdJ{S%#{4{boO^use;iPltx`Lw3QJK;+^^U3z3XzVgLXD diff --git a/src/pulpcore/cli/npm/__pycache__/remote.cpython-314.pyc b/src/pulpcore/cli/npm/__pycache__/remote.cpython-314.pyc index 8c274abe95d43e7546eddedae5afbd05661cb889..95595733cddeb9f73c06115c7d2bbb9346689973 100644 GIT binary patch delta 270 zcmeAZ=@a4A=HumJ00Jq&J(({i@)~k`FqbfUu#~WPu$D{=G?bBNh+_z1_7w6EE)fQ^ zSv*BNL`y{F8DfPePE%*(nYb^Ok$bWXqcb;qELSX_9&?FYEZ^iT##~lTMTS`3$#)pF zSp|SBIWUVA$YP#s!(_=W2oj5BnOwu<%E|_p=K!+gK`I&fCo3_lZZ2Y$Wt{BBtUbA( z0stySLY)8r delta 272 zcmeAZ=@a4A=HumJ0D|a0+cRHIMrUsBSiV>xJ?0X*SfR;TjJd45iVU%WlkYHU zvx)#&a$puGki|aPhRKp$6eJeQF}a4xm6Z!F&jV!1gH$pKPgY`9ot(;|wb_kXj&X87 z%Ns`4$u+ETjOvqbvo2?3ncU3g!6-ZV3|sl+rR?I2%#-)BO9Msi*~KQuv%9hu2?ISf m`8xX{m8T4bcNxsSvj{RWeP=)+K7+U))fo8YHt*#KWCQ?|*Fk>( diff --git a/src/pulpcore/cli/npm/__pycache__/repository.cpython-314.pyc b/src/pulpcore/cli/npm/__pycache__/repository.cpython-314.pyc index 9af777bbbbc3e1528c83e4b81799b9219294f21e..1405379a63928a81c19e8d4f9c0ad261e040c6bb 100644 GIT binary patch delta 572 zcmYk0OKTHR6vywGN9L88JldFKW}3;QRx6TeQE9soDuUdAh)ma4p<1etOhUMUF5L7Z zgd@0_wQhx3>dFt`LU;KFMGy)~ap%3osQ1F}JpTW~Ir&Du;okIIn~2w_eSGhk%lzZa1wBvl7(>T{n4JfI;DFMHZxjdpnFGS>$Y?eZ>- zc}#n}7ZKK(f4*K@iAh>Y4e?DDZvhLi1Y6+Y4lpR=Yq(!Cn8WIc%KNM)qQVp0hjwM< z4#*P;VVi7~Rj{fY=ypt)B~?6C_TUPJT7ZVOs_UY^ja{uND`=vx-i6X%wI$SKsp{tc zl~EPf`K*iI)jl-D#IE5mgVoul+|meBNY{3iKXV&&BuoiQ0Ka+LjocsM&so{dMNaVA2>k52eh?wGbE we0=2vK_Jpa}EgmZIdS1Y_dtTw4&M84@@qCMUV-IhYdZBLPDs4WH&5R@Y=Jd z@Csh`tXE+V9{e9X^yq(35TRfRf(K`U710@ZW}f%=yfbgS6Yu11<#QGh>*(jl+1FL> zaMvZziJuzk)ZoVEt`1FVaf@bnhGuzoQ`3g4S?@Nj^E$P;O&h#H9q!O3Z_*ZTQJ1^4 z&D*rYJG9HYE@8&%^KPlxA%5D|#Wq-GADD(|*oPWE00!Im1|FA6til{G#e1wFPKA5; z2f7!L8f@g7W!&8N^$&dm{IQzHCD9f#pO(vW6c2$Po? zUx6up%Efq=x5qLOsZunVtt?OG%NNJUbW%Z1 None: +def distribution(ctx: click.Context, pulp_ctx: PulpCLIContext, /, distribution_type: str) -> None: if distribution_type == "npm": ctx.obj = PulpNpmDistributionContext(pulp_ctx) else: diff --git a/src/pulpcore/cli/npm/remote.py b/src/pulpcore/cli/npm/remote.py index 99b0a3c..11fc880 100644 --- a/src/pulpcore/cli/npm/remote.py +++ b/src/pulpcore/cli/npm/remote.py @@ -1,8 +1,4 @@ import click - -from pulp_glue.common.i18n import get_translation -from pulp_glue.npm.context import PulpNpmRemoteContext - from pulp_cli.generic import ( PulpCLIContext, common_remote_create_options, @@ -22,6 +18,9 @@ update_command, ) +from pulp_glue.common.i18n import get_translation +from pulp_glue.npm.context import PulpNpmRemoteContext + translation = get_translation(__package__) _ = translation.gettext diff --git a/src/pulpcore/cli/npm/repository.py b/src/pulpcore/cli/npm/repository.py index 95fef44..a19b52d 100644 --- a/src/pulpcore/cli/npm/repository.py +++ b/src/pulpcore/cli/npm/repository.py @@ -1,19 +1,6 @@ import typing as t import click - -from pulp_glue.common.context import ( - EntityFieldDefinition, - PulpRemoteContext, - PulpRepositoryContext, -) -from pulp_glue.common.i18n import get_translation -from pulp_glue.npm.context import ( - PulpNpmPackageContentContext, - PulpNpmRemoteContext, - PulpNpmRepositoryContext, -) - from pulp_cli.generic import ( PulpCLIContext, create_command, @@ -38,6 +25,18 @@ version_command, ) +from pulp_glue.common.context import ( + EntityFieldDefinition, + PulpRemoteContext, + PulpRepositoryContext, +) +from pulp_glue.common.i18n import get_translation +from pulp_glue.npm.context import ( + PulpNpmPackageContentContext, + PulpNpmRemoteContext, + PulpNpmRepositoryContext, +) + translation = get_translation(__package__) _ = translation.gettext diff --git a/tests/test_help_pages.py b/tests/test_help_pages.py index 8e8a358..1d3da8c 100644 --- a/tests/test_help_pages.py +++ b/tests/test_help_pages.py @@ -6,6 +6,7 @@ from packaging.version import parse as parse_version from pulp_cli import __version__ as PULP_CLI_VERSION from pulp_cli import load_plugins, main +from pytest_subtests import SubTests load_plugins() @@ -39,7 +40,7 @@ def getter(self: t.Any) -> None: @pytest.mark.help_page -def test_access_help(no_api: None, subtests: pytest.Subtests) -> None: +def test_access_help(no_api: None, subtests: SubTests) -> None: """Test, that all help screens are accessible without touching the api property.""" if parse_version(PULP_CLI_VERSION) < parse_version("0.24"): pytest.skip("This test is incompatible with older cli versions.") From 3dc4440fae1368b0e372e85d9508a952d84bb5eb Mon Sep 17 00:00:00 2001 From: Yasen Date: Thu, 19 Mar 2026 16:12:08 +0100 Subject: [PATCH 3/4] fix tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e8813e..defa049 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: python: "3.11" - image_tag: "latest" lower_bounds: true - python: "3.8" + python: "3.11" steps: - uses: "actions/checkout@v6" - uses: "actions/cache@v5" From a7573f54eed26b54d7b1589325130641b73843bf Mon Sep 17 00:00:00 2001 From: Yasen Date: Thu, 19 Mar 2026 16:18:51 +0100 Subject: [PATCH 4/4] increase lower bound --- lower_bounds_constraints.lock | 4 ++-- pulp-glue-npm/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lower_bounds_constraints.lock b/lower_bounds_constraints.lock index dc7fab9..c7e011c 100644 --- a/lower_bounds_constraints.lock +++ b/lower_bounds_constraints.lock @@ -1,2 +1,2 @@ -pulp-cli==0.23.1 -pulp-glue==0.23.1 +pulp-cli==0.36.0 +pulp-glue==0.36.0 diff --git a/pulp-glue-npm/pyproject.toml b/pulp-glue-npm/pyproject.toml index 680e9a5..acc6d09 100644 --- a/pulp-glue-npm/pyproject.toml +++ b/pulp-glue-npm/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pulp-glue>=0.23.1,<0.37", + "pulp-glue>=0.36.0,<0.37", ] [project.urls] diff --git a/pyproject.toml b/pyproject.toml index bfe2485..fddf540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers=[ "Typing :: Typed", ] dependencies = [ - "pulp-cli>=0.23.1,<0.37", + "pulp-cli>=0.36.0,<0.37", "pulp-glue-npm==0.0.1.dev0", ]