From c4ccb5617863ac97632d74fa6c0053535360b9b1 Mon Sep 17 00:00:00 2001 From: abhishekmadan30 Date: Thu, 25 Jun 2026 17:49:45 -0400 Subject: [PATCH] feat!: add support to publish to huawei store --- pushapkscript/docker.d/init_worker.sh | 4 ++ pushapkscript/docker.d/worker.yml | 12 ++++ pushapkscript/examples/config.example.json | 4 ++ pushapkscript/pyproject.toml | 1 + .../src/pushapkscript/data/config_schema.json | 16 +++++ pushapkscript/src/pushapkscript/publish.py | 4 +- .../src/pushapkscript/publish_config.py | 21 ++++-- .../integration/test_integration_script.py | 51 ++++++++++++++ pushapkscript/tests/test_config.py | 4 ++ pushapkscript/tests/test_publish.py | 3 + pushapkscript/tests/test_publish_config.py | 66 +++++++++++++++++++ 11 files changed, 181 insertions(+), 5 deletions(-) diff --git a/pushapkscript/docker.d/init_worker.sh b/pushapkscript/docker.d/init_worker.sh index 441bccaaf..074a6d05d 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" + export HUAWEI_CLIENT_ID_DEP="0123456" + export HUAWEI_ACCESS_TOKEN_DEP="dummy" import_cert fenix $CERT_DIR/fenix_dep.pem import_cert focus $CERT_DIR/focus_dep.pem @@ -85,6 +87,8 @@ 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_CLIENT_ID' + test_var_set 'HUAWEI_ACCESS_TOKEN' export GOOGLE_CREDENTIALS_FOCUS_PATH=$CONFIG_DIR/focus.json export GOOGLE_CREDENTIALS_FENIX_NIGHTLY_PATH=$CONFIG_DIR/fenix_nightly.json diff --git a/pushapkscript/docker.d/worker.yml b/pushapkscript/docker.d/worker.yml index 80da70915..04ac95a8d 100644 --- a/pushapkscript/docker.d/worker.yml +++ b/pushapkscript/docker.d/worker.yml @@ -42,6 +42,9 @@ products: samsung: service_account_id: { "$eval": "SGS_SERVICE_ACCOUNT_ID" } access_token: { "$eval": "SGS_ACCESS_TOKEN" } + huawei: + client_id: { "$eval": "HUAWEI_CLIENT_ID" } + access_token: { "$eval": "HUAWEI_ACCESS_TOKEN" } - product_names: ["focus-android" ] digest_algorithm: 'SHA-256' skip_check_ordered_version_codes: true @@ -70,6 +73,9 @@ products: samsung: service_account_id: { "$eval": "SGS_SERVICE_ACCOUNT_ID" } access_token: { "$eval": "SGS_ACCESS_TOKEN" } + huawei: + client_id: { "$eval": "HUAWEI_CLIENT_ID" } + access_token: { "$eval": "HUAWEI_ACCESS_TOKEN" } klar-release: package_names: ["org.mozilla.klar"] certificate_alias: 'focus' @@ -104,6 +110,9 @@ products: samsung: service_account_id: { "$eval": "SGS_SERVICE_ACCOUNT_ID_DEP" } access_token: { "$eval": "SGS_ACCESS_TOKEN_DEP" } + huawei: + client_id: { "$eval": "HUAWEI_CLIENT_ID_DEP" } + access_token: { "$eval": "HUAWEI_ACCESS_TOKEN_DEP" } - product_names: ["focus-android" ] digest_algorithm: "SHA-256" skip_check_ordered_version_codes: true @@ -132,6 +141,9 @@ products: samsung: service_account_id: { "$eval": "SGS_SERVICE_ACCOUNT_ID_DEP" } access_token: { "$eval": "SGS_ACCESS_TOKEN_DEP" } + huawei: + client_id: { "$eval": "HUAWEI_CLIENT_ID_DEP" } + access_token: { "$eval": "HUAWEI_ACCESS_TOKEN_DEP" } klar-release: package_names: ["org.mozilla.klar"] certificate_alias: 'focus' diff --git a/pushapkscript/examples/config.example.json b/pushapkscript/examples/config.example.json index e131a3c8a..6fdaf6b1d 100644 --- a/pushapkscript/examples/config.example.json +++ b/pushapkscript/examples/config.example.json @@ -68,6 +68,10 @@ "samsung": { "sgs_service_account_id": "0123456", "sgs_access_token": "abcdef" + }, + "huawei": { + "client_id": "0123456", + "access_token": "abcdef" } } } diff --git a/pushapkscript/pyproject.toml b/pushapkscript/pyproject.toml index 8e9f2576d..f7c6a0085 100644 --- a/pushapkscript/pyproject.toml +++ b/pushapkscript/pyproject.toml @@ -13,6 +13,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ + # TODO: bump to >=12.0.0 once mozapkpublisher 12.0.0 (huawei store) is on PyPI "mozapkpublisher", "scriptworker", ] diff --git a/pushapkscript/src/pushapkscript/data/config_schema.json b/pushapkscript/src/pushapkscript/data/config_schema.json index 381f909aa..7b6018e58 100644 --- a/pushapkscript/src/pushapkscript/data/config_schema.json +++ b/pushapkscript/src/pushapkscript/data/config_schema.json @@ -141,6 +141,22 @@ "type": "string" } } + }, + "huawei": { + "type": "object", + "required": [ + "client_id", + "access_token" + ], + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string" + }, + "access_token": { + "type": "string" + } + } } } } diff --git a/pushapkscript/src/pushapkscript/publish.py b/pushapkscript/src/pushapkscript/publish.py index 34c9ca2b3..bf38c0ea7 100644 --- a/pushapkscript/src/pushapkscript/publish.py +++ b/pushapkscript/src/pushapkscript/publish.py @@ -25,7 +25,9 @@ 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_client_id=publish_config.get("huawei_client_id"), + huawei_access_token=publish_config.get("huawei_access_token"), + # 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 3905bee77..c861a9f12 100644 --- a/pushapkscript/src/pushapkscript/publish_config.py +++ b/pushapkscript/src/pushapkscript/publish_config.py @@ -2,6 +2,19 @@ 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_client_id": "client_id", + "huawei_access_token": "access_token", + }, +} + def _should_do_dry_run(task): # Don't commit anything by default. Committed APKs can't be unpublished, @@ -70,18 +83,18 @@ 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), "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/tests/integration/test_integration_script.py b/pushapkscript/tests/integration/test_integration_script.py index 068f03fe8..0b0d6d177 100644 --- a/pushapkscript/tests/integration/test_integration_script.py +++ b/pushapkscript/tests/integration/test_integration_script.py @@ -166,6 +166,10 @@ def generate_fenix_config(self): "service_account_id": "123", "access_token": "456", }, + "huawei": { + "client_id": "789", + "access_token": "abc", + }, }, }, } @@ -238,6 +242,8 @@ def test_main_fennec_style(self, push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_client_id=None, + huawei_access_token=None, submit=False, ) @@ -268,6 +274,8 @@ def test_main_focus_style(self, push_apk): skip_checks_fennec=True, sgs_service_account_id=None, sgs_access_token=None, + huawei_client_id=None, + huawei_access_token=None, submit=False, ) @@ -298,6 +306,8 @@ def test_main_fenix_style(self, push_apk): skip_checks_fennec=True, sgs_service_account_id=None, sgs_access_token=None, + huawei_client_id=None, + huawei_access_token=None, submit=False, ) @@ -328,6 +338,8 @@ 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_client_id=None, + huawei_access_token=None, submit=False, ) @@ -358,6 +370,8 @@ def test_main_allows_rollout_percentage(self, push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_client_id=None, + huawei_access_token=None, submit=False, ) @@ -389,6 +403,8 @@ def test_main_allows_commit_transaction(self, push_apk): skip_checks_fennec=False, sgs_service_account_id=None, sgs_access_token=None, + huawei_client_id=None, + huawei_access_token=None, submit=False, ) @@ -420,5 +436,40 @@ def test_main_with_samsung_store(self, push_apk): skip_checks_fennec=True, sgs_service_account_id="123", sgs_access_token="456", + huawei_client_id=None, + huawei_access_token=None, + submit=False, + ) + + @unittest.mock.patch("pushapkscript.publish.push_apk") + 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._copy_all_apks_to_test_temp_dir(task_generator) + self.keystore_manager.add_certificate("nightly") + 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_client_id="789", + huawei_access_token="abc", submit=False, ) diff --git a/pushapkscript/tests/test_config.py b/pushapkscript/tests/test_config.py index ccb11b610..f1fc30b97 100644 --- a/pushapkscript/tests/test_config.py +++ b/pushapkscript/tests/test_config.py @@ -67,6 +67,8 @@ def test_firefox_fake_prod(): "GOOGLE_CREDENTIALS_FOCUS_DEP_PATH": "focus", "SGS_SERVICE_ACCOUNT_ID_DEP": "123456", "SGS_ACCESS_TOKEN_DEP": "abcdef", + "HUAWEI_CLIENT_ID_DEP": "123456", + "HUAWEI_ACCESS_TOKEN_DEP": "abcdef", } _validate_config(context) @@ -81,5 +83,7 @@ def test_firefox_prod(): "GOOGLE_CREDENTIALS_FOCUS_PATH": "focus", "SGS_SERVICE_ACCOUNT_ID": "123456", "SGS_ACCESS_TOKEN": "abcdef", + "HUAWEI_CLIENT_ID": "123456", + "HUAWEI_ACCESS_TOKEN": "abcdef", } _validate_config(context) diff --git a/pushapkscript/tests/test_publish.py b/pushapkscript/tests/test_publish.py index 8b0227bef..1d15a57a4 100644 --- a/pushapkscript/tests/test_publish.py +++ b/pushapkscript/tests/test_publish.py @@ -43,6 +43,9 @@ 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_client_id=None, + huawei_access_token=None, + submit=False, ) async def test_publish_aab_config(self, mock_push_aab, mock_push_apk): diff --git a/pushapkscript/tests/test_publish_config.py b/pushapkscript/tests/test_publish_config.py index 6b590c594..c9558860d 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": {"client_id": "654321", "access_token": "fedcba"}, } } } @@ -200,6 +203,69 @@ 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, + "huawei_client_id": "654321", + "huawei_access_token": "fedcba", + "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, + "huawei_client_id": "654321", + "huawei_access_token": "fedcba", + "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, + "huawei_client_id": "654321", + "huawei_access_token": "fedcba", + "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, + "huawei_client_id": "654321", + "huawei_access_token": "fedcba", + "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_should_do_dry_run(): task_payload = {"commit": True} assert _should_do_dry_run(task_payload) is False