commit 0a437e60ab98a0ce22ace3522192cf31288818cf
parent 0fb9775c851781926ccb039a06ec513d5fe152bd
Author: Ben Chatterton <bchatterton@mozilla.com>
Date: Thu, 20 Nov 2025 21:07:45 +0000
Bug 1994228: Run limited update test set on push_firefox, r=taskgraph-reviewers,bhearsum
Differential Revision: https://phabricator.services.mozilla.com/D272050
Diffstat:
6 files changed, 167 insertions(+), 42 deletions(-)
diff --git a/taskcluster/config.yml b/taskcluster/config.yml
@@ -194,7 +194,7 @@ treeherder:
'Boot': 'Bootstrap'
'Attr-L10n': 'Build Attribution'
'android-l10n': 'Import strings from android-l10n repo'
- 'update-test': 'Test updates to release from older firefox versions'
+ 'updt-tst': 'Test updates to release from older firefox versions'
# The below symbols are from firefox-android (fenix/focus/android-components)
'buildconfig': 'Make sure the content of .buildconfig.yml matches what gradle knows about its projects'
diff --git a/taskcluster/gecko_taskgraph/transforms/update_test.py b/taskcluster/gecko_taskgraph/transforms/update_test.py
@@ -12,7 +12,7 @@ from taskgraph.util.copy import deepcopy
from taskgraph.util.schema import resolve_keyed_by
from typing_extensions import final
-from gecko_taskgraph.util.attributes import task_name
+from gecko_taskgraph.util.attributes import is_try, task_name
@final
@@ -65,15 +65,15 @@ def infix_treeherder_symbol(symbol, infix):
@transforms.add
def set_task_configuration(config, tasks):
release_type = ReleaseType.release
- config_tasks = {}
if config.params["release_type"] == "beta":
release_type = ReleaseType.beta
elif config.params["release_type"].startswith("esr"):
esr_version = int(config.params["release_type"].split("esr")[1])
release_type = ReleaseType.esr
if esr_version < ESR_SUPPORT_CUTOFF:
- return None
+ yield None
+ config_tasks = {}
for dep in config.kind_dependencies_tasks.values():
if "update-verify-config" in dep.kind:
config_tasks[task_name(dep)] = dep
@@ -107,17 +107,36 @@ def set_task_configuration(config, tasks):
del this_task["test-platforms"]
if this_task["shipping-product"] == "firefox":
- if release_type == ReleaseType.beta:
- this_task["name"] = this_task["name"] + "-beta"
- this_task["run"]["command"] = (
- this_task["run"]["command"] + " --channel beta-localtest"
+ product_channel = release_type.name.lower()
+ if this_task["shipping-phase"] == "promote":
+ update_channel = "-localtest"
+ elif this_task["shipping-phase"] == "push":
+ update_channel = "-cdntest"
+ else:
+ raise OSError("Expected promote or push shipping-phase.")
+
+ if this_task["shipping-phase"] != "promote":
+ this_task["treeherder"]["symbol"] = infix_treeherder_symbol(
+ this_task["treeherder"]["symbol"],
+ this_task["shipping-phase"][:6],
)
- elif release_type == ReleaseType.esr:
- this_task["name"] = this_task["name"] + "-esr"
+ if release_type != ReleaseType.release:
+ this_task["name"] = this_task["name"] + f"-{product_channel}"
+ this_task["run"]["command"] = (
+ this_task["run"]["command"]
+ + f" --channel {product_channel}{update_channel}"
+ )
+
+ if release_type == ReleaseType.esr:
this_task["run"]["command"] = (
- this_task["run"]["command"]
- + f" --channel esr-localtest --esr-version {esr_version}"
+ this_task["run"]["command"] + f" --esr-version {esr_version}"
)
+
+ if is_try(config.params):
+ this_task["run"]["command"] = (
+ this_task["run"]["command"] + " --use-balrog-staging"
+ )
+ this_task["worker"]["env"]["BALROG_STAGING"] = "1"
this_task["name"] = this_task["name"].replace("linux-docker-", "")
this_task["index"]["job-name"] = "update-test-" + this_task["name"]
@@ -152,14 +171,14 @@ def parametrize_by_locale(config, tasks):
this_task["run"]["command"] + f" --source-locale {locale}"
)
this_task["description"] = (
- f'{this_task["description"]}, locale coverage: {locale}'
+ f"{this_task['description']}, locale coverage: {locale}"
)
this_task["name"] = this_task["name"].replace("locale", locale)
this_task["index"][
"job-name"
- ] = f'{this_task["index"]["job-name"]}-{locale}'
+ ] = f"{this_task['index']['job-name']}-{locale}"
this_task["treeherder"]["symbol"] = infix_treeherder_symbol(
- this_task["treeherder"]["symbol"], locale
+ this_task["treeherder"]["symbol"], locale.replace("-", "")
)
yield this_task
@@ -190,7 +209,7 @@ def parametrize_by_source_version(config, tasks):
this_task["name"] = this_task["name"].replace("-source-version", ago_tag)
this_task["index"]["job-name"] = this_task["index"]["job-name"] + ago_tag
this_task["treeherder"]["symbol"] = infix_treeherder_symbol(
- this_task["treeherder"]["symbol"], ago_tag.split("-", 2)[-1]
+ this_task["treeherder"]["symbol"], "oldst" if v == 0 else f"bk{v}"
)
yield this_task
diff --git a/taskcluster/kinds/update-test/kind.yml b/taskcluster/kinds/update-test/kind.yml
@@ -20,13 +20,12 @@ transforms:
task-defaults:
run-on-repo-type: [hg]
- shipping-phase: promote
shipping-product: firefox
run-on-releases: [release, release-rc, beta, esr140]
treeherder:
kind: other
tier: 2
- symbol: update-test(firefox)
+ symbol: updt-tst(fx)
worker:
max-run-time: 10800
artifacts:
@@ -54,6 +53,7 @@ task-defaults:
tasks:
update-test-locale-fx:
+ shipping-phase: promote
test-platforms:
- windows11-64-24h2-shippable
- windows10-64-2009-shippable-qr
@@ -62,6 +62,7 @@ tasks:
- macosx1015-64-shippable-qr
update-test-linux-docker-locale-fx:
+ shipping-phase: promote
worker:
artifacts:
- type: directory
@@ -77,6 +78,7 @@ tasks:
- linux2404-64-shippable
update-test-source-version-fx:
+ shipping-phase: promote
test-platforms:
- windows11-64-24h2-shippable
- windows10-64-2009-shippable-qr
@@ -85,6 +87,7 @@ tasks:
- macosx1015-64-shippable-qr
update-test-linux-docker-source-version-fx:
+ shipping-phase: promote
worker:
artifacts:
- type: directory
@@ -100,6 +103,7 @@ tasks:
- linux2404-64-shippable
update-test-fx-bkg:
+ shipping-phase: promote
run:
command: >-
./mach update-test --test-type Background
@@ -111,6 +115,7 @@ tasks:
- macosx1015-64-shippable-qr
update-test-linux-docker-fx-bkg:
+ shipping-phase: promote
worker:
artifacts:
- type: directory
@@ -124,3 +129,28 @@ tasks:
. $HOME/scripts/xvfb.sh && start_xvfb '1600x1200x24' 0 && ./mach update-test --test-type Background
test-platforms:
- linux2404-64-shippable
+
+ update-test-push-locale-fx:
+ run-on-releases: [release, esr140]
+ shipping-phase: push
+ test-platforms:
+ - windows11-64-24h2-shippable
+ - macosx1500-aarch64-shippable
+
+ update-test-linux-docker-push-locale-fx:
+ run-on-releases: [release, esr140]
+ shipping-phase: push
+ worker-type: t-linux-docker-amd
+ worker:
+ artifacts:
+ - type: directory
+ path: /builds/worker/artifacts/update-test
+ name: public/update-test
+ env:
+ UPLOAD_DIR: /builds/worker/artifacts
+ VERSION_LOG_FILENAME: update-test-version-info.log
+ run:
+ command: >-
+ . $HOME/scripts/xvfb.sh && start_xvfb '1600x1200x24' 0 && ./mach update-test
+ test-platforms:
+ - linux2404-64-shippable
diff --git a/testing/update/mach_commands.py b/testing/update/mach_commands.py
@@ -2,6 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, # You can obtain one at http://mozilla.org/MPL/2.0/.
+import json
import logging
import re
import subprocess
@@ -21,6 +22,12 @@ from mozbuild.base import BinaryNotFoundException
from mozlog.structured import commandline
from mozrelease.update_verify import UpdateVerifyConfig
+STAGING_POLICY_PAYLOAD = {
+ "policies": {
+ "AppUpdateURL": "https://stage.balrog.nonprod.cloudops.mozgcp.net/update/6/Firefox/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml"
+ }
+}
+
@dataclass
class UpdateTestConfig:
@@ -40,6 +47,7 @@ class UpdateTestConfig:
config_source = None
release_type: ReleaseType = ReleaseType.release
esr_version = None
+ staging_update = False
def __post_init__(self):
if environ.get("UPLOAD_DIR"):
@@ -165,8 +173,16 @@ def get_valid_source_versions(config):
"""
ftp_content = requests.get(config.ftp_server).content.decode()
# All versions start with e.g. 140.0, so beta and release can be int'ed
- latest_version = int(config.target_version.split(".")[0])
- major_minor = ".".join(config.target_version.split(".")[:1])
+ ver_head, ver_tail = config.target_version.split(".", 1)
+ latest_version = int(ver_head)
+ latest_minor_str = ""
+ # Versions like 130.10.1 and 130.0 are possible, capture the minor number
+ for c in ver_tail:
+ try:
+ int(c)
+ latest_minor_str = latest_minor_str + c
+ except ValueError:
+ break
valid_versions: list[str] = []
for major in range(latest_version - config.major_version_range, latest_version + 1):
@@ -174,17 +190,20 @@ def get_valid_source_versions(config):
if config.release_type == ReleaseType.esr and major != latest_version:
continue
for minor in range(0, 11):
- if f"{major}.{minor}" == major_minor:
- break
- elif f"/{major}.{minor}/" in ftp_content:
- minor_versions.append(minor)
- valid_versions.append(f"{major}.{minor}")
- elif (
- config.release_type == ReleaseType.esr
- and f"/{major}.{minor}esr/" in ftp_content
+ if (
+ config.release_type == ReleaseType.release
+ and f"/{major}.{minor}/" in ftp_content
):
+ if f"{major}.{minor}" == config.target_version:
+ break
minor_versions.append(minor)
valid_versions.append(f"{major}.{minor}")
+ elif config.release_type == ReleaseType.esr and re.compile(
+ rf"/{major}\.{minor}.*/"
+ ).search(ftp_content):
+ minor_versions.append(minor)
+ if f"/{major}.{minor}esr" in ftp_content:
+ valid_versions.append(f"{major}.{minor}")
elif config.release_type == ReleaseType.beta and minor == 0:
# Release 1xx.0 is not available, but 1xx.0b1 is:
minor_versions.append(minor)
@@ -192,7 +211,7 @@ def get_valid_source_versions(config):
sep = "b" if config.release_type == ReleaseType.beta else "."
for minor in minor_versions:
- for dot in range(1, 15):
+ for dot in range(0, 15):
if f"{major}.{minor}{sep}{dot}" == config.target_version:
break
if config.release_type == ReleaseType.esr:
@@ -275,6 +294,25 @@ def get_binary_path(config: UpdateTestConfig, **kwargs) -> str:
fh.write(response.content)
fx_location = mozinstall.install(installer_filename, installed_app_dir)
print(f"Firefox installed to {fx_location}")
+
+ if config.staging_update:
+ print("Writing enterprise policy for update server")
+ fx_path = Path(fx_location)
+ policy_path = None
+ if mozinfo.os in ["linux", "win"]:
+ policy_path = fx_path / "distribution"
+ elif mozinfo.os == "mac":
+ policy_path = fx_path / "Contents" / "Resources" / "distribution"
+ else:
+ raise ValueError("Invalid OS.")
+ makedirs(policy_path)
+ policy_loc = policy_path / "policies.json"
+ print(f"Creating {policy_loc}...")
+ with policy_loc.open("w") as fh:
+ json.dump(STAGING_POLICY_PAYLOAD, fh, indent=2)
+ with policy_loc.open() as fh:
+ print(fh.read())
+
return fx_location
@@ -299,6 +337,9 @@ def get_binary_path(config: UpdateTestConfig, **kwargs) -> str:
help="ESR version, if set with --channel=esr, will only update within ESR major version",
)
@CommandArgument("--uv-config-file", help="Update Verify config file")
+@CommandArgument(
+ "--use-balrog-staging", action="store_true", help="Update from staging, not prod"
+)
def build(command_context, binary_path, **kwargs):
config = UpdateTestConfig()
@@ -337,6 +378,9 @@ def build(command_context, binary_path, **kwargs):
config.tempdir = tempdir_name
test_type = kwargs.get("test_type")
+ if kwargs.get("use_balrog_staging"):
+ config.staging_update = True
+
# Select update channel
if kwargs.get("channel"):
config.set_channel(kwargs["channel"], kwargs.get("esr_version"))
@@ -491,6 +535,9 @@ def bits_pretest():
def bits_posttest(config):
+ if config.staging_update:
+ # If we are in try, we didn't run the full test and BITS will fail.
+ return None
config.log_file_path.close()
sys.stdout = sys.__stdout__
@@ -516,7 +563,7 @@ def bits_posttest(config):
)
except (UnicodeDecodeError, AssertionError) as e:
failed = 1
- print(e)
+ logging.error(e.__traceback__)
finally:
Path(config.log_file_path.name).unlink()
diff --git a/testing/update/test_apply_update.py b/testing/update/test_apply_update.py
@@ -25,10 +25,8 @@ class TestApplyUpdate(MarionetteTestCase):
update_url = self.marionette.execute_async_script(
"""
(async function() {
- let { UpdateUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/UpdateUtils.sys.mjs"
- );
- let url = await UpdateUtils.formatUpdateURL(Services.appinfo.updateURL);
+ const checker = Cc["@mozilla.org/updates/update-checker;1"].getService(Ci.nsIUpdateChecker);
+ let url = await checker.wrappedJSObject.getUpdateURL(checker.BACKGROUND_CHECK);
return url;
})().then(arguments[0]);
"""
@@ -52,11 +50,24 @@ class TestApplyUpdate(MarionetteTestCase):
self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
initial_ver = self.marionette.find_element(By.ID, "version").text
+ # Try runs build unsigned updates, releases can't update on unsigned MARs
+ # ...so we're just going to check that balrog gives a reasonably-named file that exists
+ if environ.get("BALROG_STAGING"):
+ print("staging")
+ patch_url = root[0][0].get("URL")
+ assert (
+ f"{target_ver}" in patch_url
+ ), f"{target_ver} not in patch url: {patch_url}"
+ patch_response = requests.get(patch_url)
+ patch_response.raise_for_status()
+ return True
+
Wait(self.marionette, timeout=10).until(
expected.element_displayed(By.ID, "downloadAndInstallButton")
)
self.marionette.find_element(By.ID, "downloadAndInstallButton").click()
+ # Long timeouts are a known issue - Bug 2000040
Wait(self.marionette, timeout=240).until(
expected.element_displayed(By.ID, "updateButton")
)
@@ -70,6 +81,7 @@ class TestApplyUpdate(MarionetteTestCase):
self.marionette.set_pref("remote.log.level", "Trace")
self.marionette.set_pref("remote.system-access-check.enabled", False)
self.marionette.navigate(self.about_fx_url)
+
Wait(self.marionette, timeout=240).until(
expected.element_displayed(By.ID, "noUpdatesFound")
)
diff --git a/testing/update/test_background_update.py b/testing/update/test_background_update.py
@@ -9,18 +9,22 @@ from marionette_driver.wait import Wait
from marionette_harness import MarionetteTestCase
+def get_update_server_response(update_url, force: int):
+ response = requests.get(f"{update_url}?force={force}")
+ if response.status_code != 200:
+ raise Exception(
+ f"Tried to fetch update.xml but got response code {response.status_code}"
+ )
+
+ return ET.fromstring(response.text)
+
+
def get_possible_target_versions(update_url):
"""If throttled to a lower target version, return both possible versions"""
versions = []
for n in range(2):
- response = requests.get(f"{update_url}?force={n}")
- if response.status_code != 200:
- raise Exception(
- f"Tried to fetch update.xml but got response code {response.status_code}"
- )
-
# Get the target version
- root = ET.fromstring(response.text)
+ root = get_update_server_response(update_url, n)
versions.append(root[0].get("appVersion"))
return list(set(versions))
@@ -63,10 +67,24 @@ class TestBackgroundUpdate(MarionetteTestCase):
fh.write(f"Target version options: {', '.join(target_vers)}\n")
# Wait for the background update to be ready by checking for popup
+ # Long timeouts are a known issue - Bug 2000040
Wait(self.marionette, timeout=100).until(
lambda _: self.marionette.find_elements(By.ID, "appMenu-notification-popup")
)
+ # Try runs build unsigned updates, releases can't update on unsigned MARs
+ # ...so we're just going to check that balrog gives a reasonably-named file that exists
+ # We run the code here to maximize what gets run in try.
+ if environ.get("BALROG_STAGING"):
+ root = get_update_server_response(update_url, 1)
+ patch_url = root[0][0].get("URL")
+ assert (
+ f"/{target_vers[-1]}-candidates" in patch_url
+ ), f'"/{target_vers[-1]}-candidates not in patch url: {patch_url}'
+ patch_response = requests.get(patch_url)
+ patch_response.raise_for_status()
+ return True
+
# Dismiss the popup
self.marionette.find_element(By.ID, "urlbar-input").click()
self.marionette.find_element(By.ID, "PanelUI-menu-button").click()
@@ -126,5 +144,4 @@ class TestBackgroundUpdate(MarionetteTestCase):
)
def tearDown(self):
-
MarionetteTestCase.tearDown(self)