diff --git a/pushapkscript/docker.d/init_worker.sh b/pushapkscript/docker.d/init_worker.sh index 441bccaaf..ef5535402 100644 --- a/pushapkscript/docker.d/init_worker.sh +++ b/pushapkscript/docker.d/init_worker.sh @@ -73,6 +73,8 @@ case $COT_PRODUCT in export GOOGLE_CREDENTIALS_FOCUS_DEP_PATH=$CONFIG_DIR/fake_cert.json export SGS_SERVICE_ACCOUNT_ID_DEP="0123456" export SGS_ACCESS_TOKEN_DEP="dummy" + echo '{"key_id": "dummy", "sub_account": "dummy", "private_key": "dummy"}' > $CONFIG_DIR/huawei_dep.json + export HUAWEI_CREDENTIALS_DEP_PATH=$CONFIG_DIR/huawei_dep.json import_cert fenix $CERT_DIR/fenix_dep.pem import_cert focus $CERT_DIR/focus_dep.pem @@ -85,6 +87,7 @@ case $COT_PRODUCT in test_var_set 'GOOGLE_SERVICE_ACCOUNT_FENIX_RELEASE' test_var_set 'SGS_SERVICE_ACCOUNT_ID' test_var_set 'SGS_ACCESS_TOKEN' + test_var_set 'HUAWEI_SERVICE_ACCOUNT' export GOOGLE_CREDENTIALS_FOCUS_PATH=$CONFIG_DIR/focus.json export GOOGLE_CREDENTIALS_FENIX_NIGHTLY_PATH=$CONFIG_DIR/fenix_nightly.json @@ -96,6 +99,9 @@ case $COT_PRODUCT in echo $GOOGLE_SERVICE_ACCOUNT_FENIX_BETA | base64 -d > $GOOGLE_CREDENTIALS_FENIX_BETA_PATH echo $GOOGLE_SERVICE_ACCOUNT_FENIX_RELEASE | base64 -d > $GOOGLE_CREDENTIALS_FENIX_RELEASE_PATH + export HUAWEI_CREDENTIALS_PATH=$CONFIG_DIR/huawei.json + echo $HUAWEI_SERVICE_ACCOUNT | base64 -d > $HUAWEI_CREDENTIALS_PATH + import_cert fenix-nightly $CERT_DIR/fenix_nightly.pem import_cert fenix-beta $CERT_DIR/fenix_beta.pem import_cert fenix-release $CERT_DIR/fenix_release.pem diff --git a/pushapkscript/docker.d/worker.yml b/pushapkscript/docker.d/worker.yml index d89b72646..eba3cce40 100644 --- a/pushapkscript/docker.d/worker.yml +++ b/pushapkscript/docker.d/worker.yml @@ -51,6 +51,8 @@ products: samsung: service_account_id: { "$eval": "SGS_SERVICE_ACCOUNT_ID" } access_token: { "$eval": "SGS_ACCESS_TOKEN" } + huawei: + credentials_file: { "$eval": "HUAWEI_CREDENTIALS_PATH" } - product_names: ["focus-android" ] digest_algorithm: 'SHA1' skip_check_ordered_version_codes: true @@ -113,6 +115,8 @@ products: samsung: service_account_id: { "$eval": "SGS_SERVICE_ACCOUNT_ID_DEP" } access_token: { "$eval": "SGS_ACCESS_TOKEN_DEP" } + huawei: + credentials_file: { "$eval": "HUAWEI_CREDENTIALS_DEP_PATH" } - product_names: ["focus-android" ] digest_algorithm: "SHA1" skip_check_ordered_version_codes: true diff --git a/pushapkscript/examples/config.example.json b/pushapkscript/examples/config.example.json index e131a3c8a..0d69b2e29 100644 --- a/pushapkscript/examples/config.example.json +++ b/pushapkscript/examples/config.example.json @@ -66,8 +66,11 @@ "credentials_file": "/fenix-production.json" }, "samsung": { - "sgs_service_account_id": "0123456", - "sgs_access_token": "abcdef" + "service_account_id": "0123456", + "access_token": "abcdef" + }, + "huawei": { + "credentials_file": "/huawei.json" } } } diff --git a/pushapkscript/pyproject.toml b/pushapkscript/pyproject.toml index 8e9f2576d..d81d32302 100644 --- a/pushapkscript/pyproject.toml +++ b/pushapkscript/pyproject.toml @@ -13,7 +13,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "mozapkpublisher", + "mozapkpublisher>=12.0.0", "scriptworker", ] diff --git a/pushapkscript/src/pushapkscript/data/config_schema.json b/pushapkscript/src/pushapkscript/data/config_schema.json index 381f909aa..2aa914624 100644 --- a/pushapkscript/src/pushapkscript/data/config_schema.json +++ b/pushapkscript/src/pushapkscript/data/config_schema.json @@ -141,6 +141,18 @@ "type": "string" } } + }, + "huawei": { + "type": "object", + "required": [ + "credentials_file" + ], + "additionalProperties": false, + "properties": { + "credentials_file": { + "type": "string" + } + } } } } diff --git a/pushapkscript/src/pushapkscript/publish.py b/pushapkscript/src/pushapkscript/publish.py index 34c9ca2b3..8d9da44a7 100644 --- a/pushapkscript/src/pushapkscript/publish.py +++ b/pushapkscript/src/pushapkscript/publish.py @@ -25,7 +25,8 @@ async def publish(product_config, publish_config, apk_files, contact_server): skip_checks_fennec=bool(product_config.get("skip_checks_fennec")), sgs_service_account_id=publish_config.get("sgs_service_account_id"), sgs_access_token=publish_config.get("sgs_access_token"), - # Note that this only has an effect on SGS submissions, not google play. + huawei_credentials=publish_config.get("huawei_credentials"), + # Note that this only has an effect on SGS and Huawei submissions, not google play. submit=publish_config.get("submit", False), ) diff --git a/pushapkscript/src/pushapkscript/publish_config.py b/pushapkscript/src/pushapkscript/publish_config.py index 91eea87f4..b913b7748 100644 --- a/pushapkscript/src/pushapkscript/publish_config.py +++ b/pushapkscript/src/pushapkscript/publish_config.py @@ -2,6 +2,18 @@ log = logging.getLogger(__name__) +# Non-Google stores produce the same publish-config shape and differ only in which +# credential keys they expose. Maps target_store -> {output_key: store_config_key}. +_NON_GOOGLE_STORE_CREDENTIALS = { + "samsung": { + "sgs_service_account_id": "service_account_id", + "sgs_access_token": "access_token", + }, + "huawei": { + "huawei_credentials": "credentials_file", + }, +} + def _should_do_dry_run(task): # Don't commit anything by default. Committed APKs can't be unpublished, @@ -70,19 +82,19 @@ def _get_channel_publish_config(product_config, task): store_config = publish_config[target_store] rollout_percentage = task.get("rollout_percentage") - if target_store == "samsung": + if target_store in _NON_GOOGLE_STORE_CREDENTIALS: if task.get("google_play_track"): - raise ValueError("`google_play_track` is not allowed on the task if the target store is samsung") + raise ValueError(f"`google_play_track` is not allowed on the task if the target store is {target_store}") + credentials = {output_key: store_config[config_key] for output_key, config_key in _NON_GOOGLE_STORE_CREDENTIALS[target_store].items()} return { "target_store": target_store, "dry_run": _should_do_dry_run(task), "certificate_alias": publish_config.get("certificate_alias"), "package_names": publish_config["package_names"], "rollout_percentage": rollout_percentage, - "sgs_service_account_id": store_config["service_account_id"], - "sgs_access_token": store_config["access_token"], "submit": task.get("submit", False), + **credentials, } google_track = task.get("google_play_track", store_config["default_track"]) diff --git a/pushapkscript/src/pushapkscript/script.py b/pushapkscript/src/pushapkscript/script.py index dd6d0a9a5..44faef5a3 100755 --- a/pushapkscript/src/pushapkscript/script.py +++ b/pushapkscript/src/pushapkscript/script.py @@ -15,6 +15,14 @@ log = logging.getLogger(__name__) +# Human readable names for the stores mozapkpublisher can target, used in the warnings +# logged before a publication. +STORE_NAMES = { + "google": "Google Play", + "samsung": "the Samsung Galaxy Store", + "huawei": "the Huawei AppGallery", +} + async def async_main(context): android_product = task.extract_android_product_from_scopes(context) @@ -82,17 +90,25 @@ def _get_product_config(context, android_product): def _log_warning_forewords(contact_server, dry_run, target_store): - if contact_server: - if target_store == "google": - if not dry_run: - log.warning( - "You will publish APKs to Google Play. This action is irreversible,\ -if no error is detected either by this script or by Google Play." - ) - else: - log.warning("APKs will be submitted, but no change will not be committed.") + store_name = STORE_NAMES.get(target_store, target_store) + if contact_server and not dry_run: + log.warning( + "You will publish APKs to {}. This action is irreversible, if no error is detected either by this script or by {}.".format(store_name, store_name) + ) + elif target_store == "google": + # Google Play uploads inside an edit transaction it then declines to commit, and + # mocks the API outright when this instance may not contact the server. + if contact_server: + log.warning("APKs will be submitted to {}, but no change will be committed.".format(store_name)) + else: + log.warning("This pushapk instance is not allowed to talk to {}. *All* requests will be mocked.".format(store_name)) else: - log.warning("This pushapk instance is not allowed to talk to Google Play. *All* requests will be mocked.") + # The other stores have no transaction to leave uncommitted, so mozapkpublisher + # skips the upload rather than performing or mocking it. + if contact_server: + log.warning("Nothing will be uploaded to {}, since this is a dry run.".format(store_name)) + else: + log.warning("Nothing will be uploaded to {}, since this pushapk instance is not allowed to talk to it.".format(store_name)) def get_default_config(): diff --git a/pushapkscript/tests/integration/test_integration_script.py b/pushapkscript/tests/integration/test_integration_script.py index 97de3bd63..0c2ae33b8 100644 --- a/pushapkscript/tests/integration/test_integration_script.py +++ b/pushapkscript/tests/integration/test_integration_script.py @@ -206,6 +206,9 @@ def generate_fenix_config(self): "service_account_id": "123", "access_token": "456", }, + "huawei": { + "credentials_file": "huawei.json", + }, }, }, } @@ -261,7 +264,7 @@ def write_task_file(self, task): with open(task_file, "w") as f: json.dump(task, f) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_fennec_style(self, push_apk): task_generator = TaskGenerator() self.write_task_file(task_generator.generate_task("aurora")) @@ -287,10 +290,11 @@ def test_main_fennec_style(self, push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, submit=False, ) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_focus_style(self, push_apk): task_generator = TaskGenerator() self.write_task_file(task_generator.generate_task("focus", "production")) @@ -316,10 +320,11 @@ def test_main_focus_style(self, push_apk): skip_checks_fennec=True, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, submit=False, ) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_fenix_style(self, push_apk): task_generator = TaskGenerator() self.write_task_file(task_generator.generate_task("fenix", "nightly")) @@ -345,10 +350,11 @@ def test_main_fenix_style(self, push_apk): skip_checks_fennec=True, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, submit=False, ) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_downloads_verifies_signature_and_gives_the_right_config_to_mozapkpublisher(self, push_apk): task_generator = TaskGenerator() self.write_task_file(task_generator.generate_task("aurora")) @@ -374,10 +380,11 @@ def test_main_downloads_verifies_signature_and_gives_the_right_config_to_mozapkp skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, submit=False, ) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_allows_rollout_percentage(self, push_apk): task_generator = TaskGenerator(rollout_percentage=25) self.write_task_file(task_generator.generate_task("aurora")) @@ -403,10 +410,11 @@ def test_main_allows_rollout_percentage(self, push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, submit=False, ) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_allows_commit_transaction(self, push_apk): task_generator = TaskGenerator(should_commit_transaction=True) @@ -433,10 +441,11 @@ def test_main_allows_commit_transaction(self, push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, submit=False, ) - @unittest.mock.patch("pushapkscript.publish.push_apk") + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) def test_main_with_samsung_store(self, push_apk): task_generator = TaskGenerator(should_commit_transaction=True, store="samsung") @@ -463,6 +472,38 @@ def test_main_with_samsung_store(self, push_apk): skip_checks_fennec=True, sgs_service_account_id="123", sgs_access_token="456", + huawei_credentials=None, + submit=False, + ) + + @unittest.mock.patch("pushapkscript.publish.push_apk", autospec=True) + def test_main_with_huawei_store(self, push_apk): + task_generator = TaskGenerator(should_commit_transaction=True, store="huawei") + + self.write_task_file(task_generator.generate_task("fenix", channel="release")) + + self._prepare_apks(task_generator, "fenix-production") + main(config_path=self.config_generator.generate_fenix_config()) + + push_apk.assert_called_with( + apks=[ + MockFile("{}/work/cot/{}/public/build/target.apk".format(self.test_temp_dir, task_generator.arm_task_id)), + MockFile("{}/work/cot/{}/public/build/target.apk".format(self.test_temp_dir, task_generator.x86_task_id)), + ], + secret=None, + track=None, + expected_package_names=["org.mozilla.fenix"], + store="huawei", + rollout_percentage=None, + dry_run=False, + contact_server=True, + skip_check_multiple_locales=True, + skip_check_ordered_version_codes=False, + skip_check_same_locales=True, + skip_checks_fennec=True, + sgs_service_account_id=None, + sgs_access_token=None, + huawei_credentials="huawei.json", submit=False, ) diff --git a/pushapkscript/tests/test_config.py b/pushapkscript/tests/test_config.py index ccb11b610..ad4ac8ed1 100644 --- a/pushapkscript/tests/test_config.py +++ b/pushapkscript/tests/test_config.py @@ -67,6 +67,7 @@ def test_firefox_fake_prod(): "GOOGLE_CREDENTIALS_FOCUS_DEP_PATH": "focus", "SGS_SERVICE_ACCOUNT_ID_DEP": "123456", "SGS_ACCESS_TOKEN_DEP": "abcdef", + "HUAWEI_CREDENTIALS_DEP_PATH": "huawei", } _validate_config(context) @@ -81,5 +82,6 @@ def test_firefox_prod(): "GOOGLE_CREDENTIALS_FOCUS_PATH": "focus", "SGS_SERVICE_ACCOUNT_ID": "123456", "SGS_ACCESS_TOKEN": "abcdef", + "HUAWEI_CREDENTIALS_PATH": "huawei", } _validate_config(context) diff --git a/pushapkscript/tests/test_publish.py b/pushapkscript/tests/test_publish.py index 8b0227bef..76c86d3e0 100644 --- a/pushapkscript/tests/test_publish.py +++ b/pushapkscript/tests/test_publish.py @@ -1,20 +1,17 @@ -import unittest from unittest.mock import patch import pytest -import asyncio from pushapkscript.publish import publish, publish_aab from .helpers.mock_file import MockFile, mock_open -# TODO: refactor to pytest instead of unittest @patch("pushapkscript.publish.open", new=mock_open) -@patch("pushapkscript.publish.push_apk") -@patch("pushapkscript.publish.push_aab") +@patch("pushapkscript.publish.push_apk", autospec=True) +@patch("pushapkscript.publish.push_aab", autospec=True) @pytest.mark.asyncio -class PublishTest: - def setUp(self): +class TestPublish: + def setup_method(self): self.publish_config = { "target_store": "google", "dry_run": True, @@ -43,8 +40,27 @@ async def test_publish_config(self, mock_push_aab, mock_push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_credentials=None, + submit=False, ) + async def test_publish_huawei_config(self, mock_push_aab, mock_push_apk): + publish_config = { + "target_store": "huawei", + "dry_run": False, + "package_names": ["org.mozilla.fenix"], + "huawei_credentials": "/huawei.json", + "submit": True, + } + await publish({}, publish_config, self.apks, contact_server=True) + + _, args = mock_push_apk.call_args + assert args["store"] == "huawei" + assert args["huawei_credentials"] == "/huawei.json" + assert args["submit"] is True + # Huawei uses its own credentials file, so the Google Play secret is unset. + assert args["secret"] is None + async def test_publish_aab_config(self, mock_push_aab, mock_push_apk): await publish_aab({}, self.publish_config, self.aabs, contact_server=True) @@ -98,7 +114,7 @@ async def test_craft_push_aab_config_allows_to_contact_google_play_or_not(self, _, args = mock_push_aab.call_args assert args["contact_server"] is True - publish_aab({}, self.publish_config, self.aabs, False) + await publish_aab({}, self.publish_config, self.aabs, False) _, args = mock_push_aab.call_args assert args["contact_server"] is False diff --git a/pushapkscript/tests/test_publish_config.py b/pushapkscript/tests/test_publish_config.py index 8255d3f54..53d694754 100644 --- a/pushapkscript/tests/test_publish_config.py +++ b/pushapkscript/tests/test_publish_config.py @@ -1,3 +1,5 @@ +import pytest + from pushapkscript.publish_config import _should_do_dry_run, get_publish_config AURORA_CONFIG = { @@ -28,6 +30,7 @@ "certificate_alias": "fenix", "google": {"default_track": "internal", "credentials_file": "fenix.json"}, "samsung": {"service_account_id": "123456", "access_token": "abcdef"}, + "huawei": {"credentials_file": "huawei.json"}, } } } @@ -204,14 +207,79 @@ def test_target_samsung_submit(): } +def test_target_huawei(): + payload = {"channel": "production", "target_store": "huawei"} + + assert get_publish_config(FENIX_CONFIG, payload, "fenix") == { + "target_store": "huawei", + "dry_run": True, + "certificate_alias": "fenix", + "huawei_credentials": "huawei.json", + "package_names": ["org.mozilla.fenix"], + "rollout_percentage": None, + "submit": False, + } + + +def test_target_huawei_with_commit(): + payload = {"channel": "production", "target_store": "huawei", "commit": True} + + assert get_publish_config(FENIX_CONFIG, payload, "fenix") == { + "target_store": "huawei", + "dry_run": False, + "certificate_alias": "fenix", + "huawei_credentials": "huawei.json", + "package_names": ["org.mozilla.fenix"], + "rollout_percentage": None, + "submit": False, + } + + +def test_target_huawei_rollout(): + payload = {"channel": "production", "target_store": "huawei", "rollout_percentage": 50} + + assert get_publish_config(FENIX_CONFIG, payload, "fenix") == { + "target_store": "huawei", + "dry_run": True, + "certificate_alias": "fenix", + "huawei_credentials": "huawei.json", + "package_names": ["org.mozilla.fenix"], + "rollout_percentage": 50, + "submit": False, + } + + +def test_target_huawei_submit(): + payload = {"channel": "production", "target_store": "huawei", "submit": True} + + assert get_publish_config(FENIX_CONFIG, payload, "fenix") == { + "target_store": "huawei", + "dry_run": True, + "certificate_alias": "fenix", + "huawei_credentials": "huawei.json", + "package_names": ["org.mozilla.fenix"], + "rollout_percentage": None, + "submit": True, + } + + +def test_target_huawei_rejects_google_play_track(): + payload = {"channel": "production", "target_store": "huawei", "google_play_track": "production"} + + with pytest.raises(ValueError, match="`google_play_track` is not allowed"): + get_publish_config(FENIX_CONFIG, payload, "fenix") + + def test_certificate_alias_does_not_depend_on_the_target_store(): # The alias identifies the certificate the incoming artifact was signed with, which is # decided by the upstream signing task, so it is the same whichever store it goes to. google = get_publish_config(FENIX_CONFIG, {"channel": "production", "target_store": "google"}, "fenix") samsung = get_publish_config(FENIX_CONFIG, {"channel": "production", "target_store": "samsung"}, "fenix") + huawei = get_publish_config(FENIX_CONFIG, {"channel": "production", "target_store": "huawei"}, "fenix") assert google["certificate_alias"] == "fenix" assert samsung["certificate_alias"] == "fenix" + assert huawei["certificate_alias"] == "fenix" def test_certificate_alias_is_none_when_nothing_configures_it(): diff --git a/pushapkscript/tests/test_script.py b/pushapkscript/tests/test_script.py index 06e8f5922..a384142b4 100644 --- a/pushapkscript/tests/test_script.py +++ b/pushapkscript/tests/test_script.py @@ -153,12 +153,42 @@ def test_get_product_config(): True, False, "google", - "You will publish APKs to Google Play. This action is irreversible,\ -if no error is detected either by this script or by Google Play.", + "You will publish APKs to Google Play. This action is irreversible, if no error is detected either by this script or by Google Play.", ), - (True, True, "google", "APKs will be submitted, but no change will not be committed."), + (True, True, "google", "APKs will be submitted to Google Play, but no change will be committed."), (False, True, "google", "This pushapk instance is not allowed to talk to Google Play. *All* requests will be mocked."), (False, False, "google", "This pushapk instance is not allowed to talk to Google Play. *All* requests will be mocked."), + ( + True, + False, + "samsung", + "You will publish APKs to the Samsung Galaxy Store. This action is irreversible, " + "if no error is detected either by this script or by the Samsung Galaxy Store.", + ), + (True, True, "samsung", "Nothing will be uploaded to the Samsung Galaxy Store, since this is a dry run."), + ( + True, + False, + "huawei", + "You will publish APKs to the Huawei AppGallery. This action is irreversible, " + "if no error is detected either by this script or by the Huawei AppGallery.", + ), + # Unlike Google Play, these stores upload nothing at all when they aren't committing. + (True, True, "huawei", "Nothing will be uploaded to the Huawei AppGallery, since this is a dry run."), + ( + False, + True, + "huawei", + "Nothing will be uploaded to the Huawei AppGallery, since this pushapk instance is not allowed to talk to it.", + ), + ( + False, + False, + "huawei", + "Nothing will be uploaded to the Huawei AppGallery, since this pushapk instance is not allowed to talk to it.", + ), + # An unrecognised store falls back to its raw payload value rather than crashing. + (True, True, "amazon", "Nothing will be uploaded to amazon, since this is a dry run."), ), ) def test_log_warning_forewords(caplog, monkeypatch, is_allowed_to_push, dry_run, target_store, expected): diff --git a/tests/test_init.py b/tests/test_init.py index 3e85c236d..fa780cc84 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -73,6 +73,7 @@ "GOOGLE_SERVICE_ACCOUNT_REFERENCE_BROWSER": "Zm9vYmFyCg==", "SGS_SERVICE_ACCOUNT_ID": "Zm9vYmFyCg==", "SGS_ACCESS_TOKEN": "Zm9vYmFyCg==", + "HUAWEI_SERVICE_ACCOUNT": "Zm9vYmFyCg==", }, re.compile(r"pushflatpak:.*"): { "FLATHUB_URL": "https://flathub.example.com", diff --git a/uv.lock b/uv.lock index c6c6ab57c..d58db9e82 100644 --- a/uv.lock +++ b/uv.lock @@ -2520,7 +2520,7 @@ wheels = [ [[package]] name = "mozapkpublisher" -version = "11.0.2" +version = "12.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2528,12 +2528,12 @@ dependencies = [ { name = "google-auth" }, { name = "mozilla-version" }, { name = "pyaxmlparser" }, - { name = "pyjwt" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/01e47ead7dc9ddc8f0f827fd0f4f6b97502b27e3595780bec995321bae42/mozapkpublisher-11.0.2.tar.gz", hash = "sha256:9de539b6d348ace432a4f20bb9db06daf4820ff812700fdda14ce6e6088edcd6", size = 151855, upload-time = "2026-07-16T14:45:01.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/b7/ae15b448f826cc4d66b21540d04e4621a0b163379ac0100adbe18ffe05df/mozapkpublisher-12.0.0.tar.gz", hash = "sha256:20d8a117b40653a517bf994156966ac5aa3bc62e1ed938dbd96b9dd22a337bf8", size = 185839, upload-time = "2026-09-04T16:05:49.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/82/0b1e9c14de8dfeb3d68819496598d1bbfcfc32c075ab841eda2c61f1911d/mozapkpublisher-11.0.2-py3-none-any.whl", hash = "sha256:5f0b69af145ea21baabf07b588735568586bbe3e71d91d531a66522a26594409", size = 64369, upload-time = "2026-07-16T14:44:59.887Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d5/be4f2477047cfea4a719ba1934f6ab5d8c9b9ae5fb9bd2f11079cb731793/mozapkpublisher-12.0.0-py3-none-any.whl", hash = "sha256:b3a1f35760e0ebf22000d9f0d767649b952c0dfc0852f02b7f5e6c4111040f14", size = 94188, upload-time = "2026-09-04T16:05:48.161Z" }, ] [[package]] @@ -3038,7 +3038,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "mozapkpublisher" }, + { name = "mozapkpublisher", specifier = ">=12.0.0" }, { name = "scriptworker" }, ]