Add Firefox Nightly and Firefox Developer Edition Fleet-maintained apps (#49501)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** #49149 Adds **Firefox Nightly** and **Firefox Developer Edition** as Fleet-maintained apps for macOS and Windows (4 new FMAs). Mozilla VPN (also requested in #49149) is not included, so this doesn't close the issue. ## What's included | App | macOS | Windows | |-----|-------|---------| | Firefox Nightly | Homebrew cask `firefox@nightly`, bundle ID `org.mozilla.nightly` | MSIX `Mozilla.Firefox.Nightly.MSIX` (no exe package exists in winget) | | Firefox Developer Edition | Homebrew cask `firefox@developer-edition`, bundle ID `org.mozilla.firefoxdeveloperedition` | NSIS exe `Mozilla.Firefox.DeveloperEdition`, machine scope, `/S` | All identity fields were verified against the real installers, not catalog metadata: bundle IDs read from the DMGs' `Info.plist`, MSIX identity ("Firefox Nightly" / publisher "Mozilla Corporation") read from the package's `AppxManifest.xml`, and DevEd's registry identity (`Firefox Developer Edition (x64 en-US)`, publisher `Mozilla`, versionless DisplayName) confirmed in Mozilla's NSIS installer source at the shipped release tags. The MSIX install/uninstall scripts follow the established Slack/Claude pattern (provision machine-wide + register for the logged-on user via scheduled task). ## Pre-release version handling (macOS) Mozilla's pre-release bundles report only the base version in `CFBundleShortVersionString` (`153.0` for cask version `153.0b13`), which would make the generated patch policy flag every up-to-date install as outdated. Instead, the patch policies for these two channels compare **`bundle_version`** (`CFBundleVersion` encodes the build date, e.g. `15326.7.15`), so individual pre-release builds are distinguishable: - **Nightly**: derived directly from the build timestamp embedded in the cask version (`154.0a1,2026-07-17-09-27-13` → `15426.7.17`). No external calls. - **Developer Edition**: build date resolved from Mozilla's [buildhub](https://buildhub.moz.tools) (indexed as product `firefox`, channel `aurora`). On buildhub failure the ingester logs a warning and falls back to a base-version comparison (cycle granularity) rather than failing the run; it self-corrects on the next scheduled ingestion. The macOS validator gets a matching exception accepting `base + "b"` versions for the DevEd bundle ID. Both computed values were verified against the actual DMGs' `Info.plist`. **Known limitation (Windows):** patch detection for Developer Edition on Windows is only accurate to the release cycle (153 vs 154), not between betas (b13 vs b14) — the registry `DisplayVersion` and winget's `PackageVersion` are both the base version (`151.0`) with no beta number, and osquery's `programs` table has nothing finer to compare. Nightly on Windows is day-granular since its MSIX package version encodes the build date. Documented in #49149. ## Other changes - Tightened `firefox_uninstall.ps1` (release channel FMA): its `*Firefox*` match would have uninstalled Developer Edition (or ESR) first depending on registry enumeration order. Now matches `Mozilla Firefox*` and excludes ESR; `firefox/windows` output regenerated. - Channel-specific icons generated from the real app bundles (frontend components + website PNGs), with map keys covering both catalog and inventory names. ## Why Firefox Beta is not included Beta (requested in #49149) was deliberately dropped: - On Windows, the Beta exe registers the same identity as release Firefox (`Mozilla Firefox (x64 en-US)`), making it indistinguishable in inventory. The only distinct package is `Mozilla.Firefox.Beta.MSIX`, which has no `en-US` winget locale manifest (only `en`), which the ingester requires. - On macOS, Beta shares both the bundle ID (`org.mozilla.firefox`) and the install path (`/Applications/Firefox.app`) with release Firefox, so installing it silently replaces a release install. ## Notes for reviewers - Nightly URLs are date-stamped and refresh with the daily FMA ingestion; within-cycle Nightly darwin builds share a `CFBundleVersion` date so same-day rebuilds are indistinguishable. - DevEd's winget package lags Mozilla's releases (winget latest is 151.0 vs current 153.0b13); the FMA tracks winget as usual. - The `CFBundleVersion` format (`<major><yy>.<M>.<D>`, unpadded) is an inferred contract of Mozilla's build system, verified against both shipped DMGs; if it ever changes, patch status fails visibly (reports unpatched), not silently. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] Added/updated automated tests - [ ] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added deployment support for Mozilla Firefox Developer Edition and Firefox Nightly on macOS and Windows (including Homebrew/Winget inputs and platform-specific install/uninstall workflows). * Added new catalog entries and dedicated UI icons for both Firefox variants. * **Bug Fixes** * Improved macOS “patched” detection and version matching for Developer Edition and Nightly builds. * Enhanced Homebrew ingestion by resolving build information (with fallback when unavailable). * Refined Windows uninstallation matching to target the correct Firefox release channels and avoid ESR. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -294,6 +294,15 @@ func appExists(ctx context.Context, logger *slog.Logger, appName, uniqueAppIdent
|
||||
}
|
||||
}
|
||||
|
||||
// The Developer Edition cask version is the full beta ("153.0b13") but
|
||||
// the bundle reports only the base version ("153.0"); accept base+"b".
|
||||
if uniqueAppIdentifier == "org.mozilla.firefoxdeveloperedition" {
|
||||
if result.Version != "" && strings.HasPrefix(appVersion, result.Version+"b") {
|
||||
logger.InfoContext(ctx, "Firefox Developer Edition detected - cask version matches bundle base version with beta suffix")
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check various version matching strategies
|
||||
if checkVersionMatch(appVersion, result.Version, result.BundledVersion) {
|
||||
return true, nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package homebrew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +35,7 @@ func IngestApps(ctx context.Context, logger *slog.Logger, inputsPath, slugFilter
|
||||
|
||||
i := &brewIngester{
|
||||
baseURL: baseBrewAPIURL,
|
||||
buildhubURL: buildhubAPIURL,
|
||||
logger: logger,
|
||||
client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)),
|
||||
retryInterval: 2 * time.Second,
|
||||
@@ -91,12 +94,17 @@ func IngestApps(ctx context.Context, logger *slog.Logger, inputsPath, slugFilter
|
||||
return manifestApps, nil
|
||||
}
|
||||
|
||||
const baseBrewAPIURL = "https://formulae.brew.sh/api/"
|
||||
const (
|
||||
baseBrewAPIURL = "https://formulae.brew.sh/api/"
|
||||
// buildhubAPIURL is Mozilla's build metadata search API.
|
||||
buildhubAPIURL = "https://buildhub.moz.tools/api/search"
|
||||
)
|
||||
|
||||
type brewIngester struct {
|
||||
baseURL string
|
||||
logger *slog.Logger
|
||||
client *http.Client
|
||||
baseURL string
|
||||
buildhubURL string
|
||||
logger *slog.Logger
|
||||
client *http.Client
|
||||
|
||||
// retryInterval and retryMaxAttempts control retries of transient brew API
|
||||
// failures (network errors and 5xx/429 responses). formulae.brew.sh is
|
||||
@@ -239,10 +247,195 @@ func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintain
|
||||
out.UniqueIdentifier, out.Version,
|
||||
)
|
||||
}
|
||||
if input.Token == "firefox@developer-edition" {
|
||||
// The bundle reports only the base version ("153.0") for cask version
|
||||
// "153.0b13", so compare CFBundleVersion (encodes the build date, resolved
|
||||
// via buildhub) to distinguish betas; fall back to a cycle-granular
|
||||
// base-version comparison if buildhub is unavailable.
|
||||
column := "bundle_version"
|
||||
patchVersion, err := i.firefoxDevEditionMacBundleVersion(ctx, out.Version)
|
||||
if err != nil {
|
||||
i.logger.WarnContext(ctx, "resolving Firefox Developer Edition bundle version failed; patch policy falls back to base-version comparison", "err", err.Error())
|
||||
column, patchVersion = "bundle_short_version", firefoxBetaBaseVersion(out.Version)
|
||||
}
|
||||
out.Queries.Patched = fmt.Sprintf(
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(%s, '%s') < 0);",
|
||||
out.UniqueIdentifier, column, patchVersion,
|
||||
)
|
||||
}
|
||||
if input.Token == "firefox@nightly" {
|
||||
// Nightly's CFBundleShortVersionString ("154.0a1") is constant all cycle;
|
||||
// derive CFBundleVersion from the cask version's build timestamp for
|
||||
// day-level patch status.
|
||||
bundleVersion, err := firefoxNightlyMacBundleVersion(cask.Version)
|
||||
if err != nil {
|
||||
i.logger.WarnContext(ctx, "deriving Firefox Nightly bundle version failed; patch policy falls back to short-version comparison", "err", err.Error())
|
||||
} else {
|
||||
out.Queries.Patched = fmt.Sprintf(
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(bundle_version, '%s') < 0);",
|
||||
out.UniqueIdentifier, bundleVersion,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
var firefoxBetaVersionPattern = regexp.MustCompile(`^(\d+(?:\.\d+)*)b\d+$`)
|
||||
|
||||
// firefoxBetaBaseVersion strips the beta suffix from a Firefox pre-release
|
||||
// version ("153.0b13" -> "153.0"); non-matching versions pass through unchanged.
|
||||
func firefoxBetaBaseVersion(version string) string {
|
||||
if m := firefoxBetaVersionPattern.FindStringSubmatch(version); m != nil {
|
||||
return m[1]
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// firefoxMacBundleVersion computes a Firefox mac build's CFBundleVersion:
|
||||
// "<major><yy>.<month>.<day>", unpadded ("153.0b13" + "20260715" -> "15326.7.15").
|
||||
func firefoxMacBundleVersion(version, buildDate string) (string, error) {
|
||||
major, _, _ := strings.Cut(version, ".")
|
||||
if major == "" || strings.Trim(major, "0123456789") != "" {
|
||||
return "", fmt.Errorf("cannot parse major version from %q", version)
|
||||
}
|
||||
if len(buildDate) < 8 {
|
||||
return "", fmt.Errorf("invalid build date %q", buildDate)
|
||||
}
|
||||
date, err := time.Parse("20060102", buildDate[:8])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid build date %q", buildDate)
|
||||
}
|
||||
yy := buildDate[2:4]
|
||||
return fmt.Sprintf("%s%s.%d.%d", major, yy, int(date.Month()), date.Day()), nil
|
||||
}
|
||||
|
||||
// firefoxNightlyCaskVersionPattern extracts the build timestamp from a Firefox
|
||||
// Nightly cask version ("154.0a1,2026-07-17-09-27-13").
|
||||
var firefoxNightlyCaskVersionPattern = regexp.MustCompile(`^[^,]+,(\d{4})-(\d{2})-(\d{2})(?:-|$)`)
|
||||
|
||||
// firefoxNightlyMacBundleVersion derives CFBundleVersion from a Nightly cask
|
||||
// version ("154.0a1,2026-07-17-09-27-13" -> "15426.7.17").
|
||||
func firefoxNightlyMacBundleVersion(caskVersion string) (string, error) {
|
||||
m := firefoxNightlyCaskVersionPattern.FindStringSubmatch(caskVersion)
|
||||
if m == nil {
|
||||
return "", fmt.Errorf("cask version %q has no build timestamp", caskVersion)
|
||||
}
|
||||
return firefoxMacBundleVersion(caskVersion, m[1]+m[2]+m[3])
|
||||
}
|
||||
|
||||
// firefoxDevEditionMacBundleVersion resolves a Developer Edition mac build's
|
||||
// CFBundleVersion ("153.0b13" -> "15326.7.15") by looking up its build id in
|
||||
// buildhub, where DevEd is indexed as product "firefox", channel "aurora".
|
||||
func (i *brewIngester) firefoxDevEditionMacBundleVersion(ctx context.Context, version string) (string, error) {
|
||||
type term map[string]map[string]string
|
||||
reqBody, err := json.Marshal(map[string]any{
|
||||
"size": 1,
|
||||
"query": map[string]any{
|
||||
"bool": map[string]any{
|
||||
"must": []term{
|
||||
{"term": {"source.product": "firefox"}},
|
||||
{"term": {"target.channel": "aurora"}},
|
||||
{"term": {"target.platform": "mac"}},
|
||||
{"term": {"target.version": version}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"sort": []term{{"build.id": {"order": "desc"}}},
|
||||
})
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "marshal buildhub query")
|
||||
}
|
||||
|
||||
interval := i.retryInterval
|
||||
if interval <= 0 {
|
||||
interval = 2 * time.Second
|
||||
}
|
||||
maxAttempts := i.retryMaxAttempts
|
||||
if maxAttempts <= 0 {
|
||||
maxAttempts = 5
|
||||
}
|
||||
|
||||
var body []byte
|
||||
attempt := 0
|
||||
err = retry.Do(func() error {
|
||||
attempt++
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, i.buildhubURL, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create buildhub http request")
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := i.client.Do(req)
|
||||
if err != nil {
|
||||
// Caller cancellation/deadline is not transient; stop retrying.
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
i.logger.WarnContext(ctx, "buildhub request failed, retrying", "attempt", attempt, "err", err.Error())
|
||||
return &transientErr{ctxerr.Wrap(ctx, err, "execute buildhub http request")}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err = io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
i.logger.WarnContext(ctx, "reading buildhub response failed, retrying", "attempt", attempt, "err", err.Error())
|
||||
return &transientErr{ctxerr.Wrap(ctx, err, "read buildhub response body")}
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusTooManyRequests,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusBadGateway,
|
||||
http.StatusServiceUnavailable,
|
||||
http.StatusGatewayTimeout:
|
||||
i.logger.WarnContext(ctx, "buildhub returned transient error, retrying", "attempt", attempt, "status", res.StatusCode)
|
||||
return &transientErr{ctxerr.Errorf(ctx, "buildhub returned status %d: %s", res.StatusCode, truncateBody(body))}
|
||||
default:
|
||||
return ctxerr.Errorf(ctx, "buildhub returned status %d: %s", res.StatusCode, truncateBody(body))
|
||||
}
|
||||
},
|
||||
retry.WithInterval(interval),
|
||||
retry.WithBackoffMultiplier(2),
|
||||
retry.WithMaxAttempts(maxAttempts),
|
||||
retry.WithErrorFilter(func(err error) retry.ErrorOutcome {
|
||||
if _, ok := errors.AsType[*transientErr](err); ok {
|
||||
return retry.ErrorOutcomeNormalRetry
|
||||
}
|
||||
return retry.ErrorOutcomeDoNotRetry
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Hits struct {
|
||||
Hits []struct {
|
||||
Source struct {
|
||||
Build struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"build"`
|
||||
} `json:"_source"`
|
||||
} `json:"hits"`
|
||||
} `json:"hits"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "unmarshal buildhub response")
|
||||
}
|
||||
if len(resp.Hits.Hits) == 0 {
|
||||
return "", ctxerr.Errorf(ctx, "no buildhub build found for version %s", version)
|
||||
}
|
||||
|
||||
return firefoxMacBundleVersion(version, resp.Hits.Hits[0].Source.Build.ID)
|
||||
}
|
||||
|
||||
// fetchCask resolves the brew cask JSON for the given input app from
|
||||
// either a local file (cask_path) or the default brew API.
|
||||
func (i *brewIngester) fetchCask(ctx context.Context, input inputApp) (brewCask, error) {
|
||||
|
||||
@@ -95,6 +95,22 @@ func TestIngestValidations(t *testing.T) {
|
||||
Version: "1.0",
|
||||
}
|
||||
|
||||
case "firefox@developer-edition":
|
||||
cask = brewCask{
|
||||
Token: appToken,
|
||||
Name: []string{"Mozilla Firefox Developer Edition"},
|
||||
URL: "https://example.com",
|
||||
Version: "153.0b13",
|
||||
}
|
||||
|
||||
case "firefox@nightly":
|
||||
cask = brewCask{
|
||||
Token: appToken,
|
||||
Name: []string{"Mozilla Firefox Nightly"},
|
||||
URL: "https://example.com",
|
||||
Version: "154.0a1,2026-07-17-09-27-13",
|
||||
}
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
t.Fatalf("unexpected app token %s", appToken)
|
||||
@@ -105,6 +121,12 @@ func TestIngestValidations(t *testing.T) {
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
// buildhub stub: DevEd 153.0b13 build id -> CFBundleVersion "15326.7.15".
|
||||
buildhubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"hits":{"hits":[{"_source":{"build":{"id":"20260715125817"}}}]}}`))
|
||||
}))
|
||||
t.Cleanup(buildhubSrv.Close)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cases := []struct {
|
||||
@@ -121,6 +143,8 @@ func TestIngestValidations(t *testing.T) {
|
||||
{"parse URL for cask invalidurl", inputApp{Token: "invalidurl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"", inputApp{Token: "ok", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"", inputApp{Token: "docker-desktop", UniqueIdentifier: "com.electron.dockerdesktop", InstallerFormat: "dmg", Name: "Docker Desktop", Slug: "docker-desktop/darwin"}},
|
||||
{"", inputApp{Token: "firefox@developer-edition", UniqueIdentifier: "org.mozilla.firefoxdeveloperedition", InstallerFormat: "dmg", Name: "Mozilla Firefox Developer Edition", Slug: "firefox@developer-edition/darwin"}},
|
||||
{"", inputApp{Token: "firefox@nightly", UniqueIdentifier: "org.mozilla.nightly", InstallerFormat: "dmg", Name: "Mozilla Firefox Nightly", Slug: "firefox@nightly/darwin"}},
|
||||
{"", inputApp{Token: "swiftdialog", UniqueIdentifier: "au.csiro.dialog", InstallerFormat: "pkg", Name: "swiftDialog", Slug: "swiftdialog/darwin"}},
|
||||
{"", inputApp{Token: "install_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", InstallScriptPath: path.Join(tempDir, "install_script.sh")}},
|
||||
{"", inputApp{Token: "uninstall_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh")}},
|
||||
@@ -133,6 +157,7 @@ func TestIngestValidations(t *testing.T) {
|
||||
logger: slog.New(slog.DiscardHandler),
|
||||
client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)),
|
||||
baseURL: srv.URL + "/",
|
||||
buildhubURL: buildhubSrv.URL,
|
||||
retryInterval: time.Millisecond,
|
||||
retryMaxAttempts: 3,
|
||||
}
|
||||
@@ -160,6 +185,21 @@ func TestIngestValidations(t *testing.T) {
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.electron.dockerdesktop' AND path NOT LIKE '%.back' AND version_compare(bundle_short_version, '1.0') < 0);",
|
||||
out.Queries.Patched,
|
||||
)
|
||||
case "firefox@developer-edition":
|
||||
// Patched query compares the buildhub-resolved CFBundleVersion.
|
||||
require.Equal(t, "153.0b13", out.Version)
|
||||
require.Equal(t,
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition' AND version_compare(bundle_version, '15326.7.15') < 0);",
|
||||
out.Queries.Patched,
|
||||
)
|
||||
case "firefox@nightly":
|
||||
// Patched query compares the CFBundleVersion derived from the cask
|
||||
// version's build timestamp.
|
||||
require.Equal(t, "154.0a1", out.Version)
|
||||
require.Equal(t,
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly' AND version_compare(bundle_version, '15426.7.17') < 0);",
|
||||
out.Queries.Patched,
|
||||
)
|
||||
case "swiftdialog":
|
||||
require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'au.csiro.dialog' AND path != '/opt/orbit/bin/swiftDialog/macos/stable/Dialog.app';", out.Queries.Exists)
|
||||
require.Equal(t,
|
||||
@@ -361,3 +401,131 @@ func TestIngestCaskPath(t *testing.T) {
|
||||
require.ErrorContains(t, err, "empty name")
|
||||
require.Equal(t, 0, httpHits)
|
||||
}
|
||||
|
||||
func TestFirefoxBetaBaseVersion(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"153.0b13", "153.0"},
|
||||
{"154.0b1", "154.0"},
|
||||
{"153.0.1b2", "153.0.1"},
|
||||
{"153.0", "153.0"},
|
||||
{"152.0.6", "152.0.6"},
|
||||
{"154.0a1", "154.0a1"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
require.Equal(t, c.want, firefoxBetaBaseVersion(c.in), "input %q", c.in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirefoxMacBundleVersion(t *testing.T) {
|
||||
cases := []struct {
|
||||
version string
|
||||
buildDate string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"153.0b13", "20260715125817", "15326.7.15", false},
|
||||
{"154.0a1", "20260717", "15426.7.17", false},
|
||||
{"153.0.1b2", "20261201000000", "15326.12.1", false},
|
||||
{"153.0b13", "2026071", "", true}, // build date too short
|
||||
{"153.0b13", "2026x715", "", true}, // build date not numeric
|
||||
{"153.0b13", "20261315000000", "", true}, // month out of range
|
||||
{"153.0b13", "20260732000000", "", true}, // day out of range
|
||||
{"153.0b13", "20260231000000", "", true}, // impossible calendar date
|
||||
{"x.0b13", "20260715125817", "", true}, // non-numeric major
|
||||
{"", "20260715125817", "", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := firefoxMacBundleVersion(c.version, c.buildDate)
|
||||
if c.wantErr {
|
||||
require.Error(t, err, "version %q buildDate %q", c.version, c.buildDate)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, "version %q buildDate %q", c.version, c.buildDate)
|
||||
require.Equal(t, c.want, got, "version %q buildDate %q", c.version, c.buildDate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirefoxNightlyMacBundleVersion(t *testing.T) {
|
||||
cases := []struct {
|
||||
caskVersion string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"154.0a1,2026-07-17-09-27-13", "15426.7.17", false},
|
||||
{"154.0a1,2026-07-17", "15426.7.17", false},
|
||||
{"154.0a1", "", true}, // no build timestamp
|
||||
{"154.0a1,not-a-date", "", true}, // malformed timestamp
|
||||
{"154.0a1,2026-13-17-09-27-13", "", true}, // month out of range
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := firefoxNightlyMacBundleVersion(c.caskVersion)
|
||||
if c.wantErr {
|
||||
require.Error(t, err, "caskVersion %q", c.caskVersion)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, "caskVersion %q", c.caskVersion)
|
||||
require.Equal(t, c.want, got, "caskVersion %q", c.caskVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFirefoxDevEditionBuildhubFallback verifies that a buildhub failure falls
|
||||
// back to a base-version patch comparison instead of failing ingestion.
|
||||
func TestFirefoxDevEditionBuildhubFallback(t *testing.T) {
|
||||
brewSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := json.NewEncoder(w).Encode(brewCask{
|
||||
Token: "firefox@developer-edition",
|
||||
Name: []string{"Mozilla Firefox Developer Edition"},
|
||||
URL: "https://example.com",
|
||||
Version: "153.0b13",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("encoding fixture: %v", err)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(brewSrv.Close)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
}{
|
||||
{"buildhub has no matching build", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"hits":{"hits":[]}}`))
|
||||
}},
|
||||
{"buildhub is unavailable", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
buildhubSrv := httptest.NewServer(c.handler)
|
||||
t.Cleanup(buildhubSrv.Close)
|
||||
|
||||
i := &brewIngester{
|
||||
logger: slog.New(slog.DiscardHandler),
|
||||
client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)),
|
||||
baseURL: brewSrv.URL + "/",
|
||||
buildhubURL: buildhubSrv.URL,
|
||||
retryInterval: time.Millisecond,
|
||||
retryMaxAttempts: 2,
|
||||
}
|
||||
|
||||
out, err := i.ingestOne(context.Background(), inputApp{
|
||||
Token: "firefox@developer-edition",
|
||||
UniqueIdentifier: "org.mozilla.firefoxdeveloperedition",
|
||||
InstallerFormat: "dmg",
|
||||
Name: "Mozilla Firefox Developer Edition",
|
||||
Slug: "firefox@developer-edition/darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "153.0b13", out.Version)
|
||||
require.Equal(t,
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition' AND version_compare(bundle_short_version, '153.0') < 0);",
|
||||
out.Queries.Patched,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Mozilla Firefox Developer Edition",
|
||||
"slug": "firefox@developer-edition/darwin",
|
||||
"unique_identifier": "org.mozilla.firefoxdeveloperedition",
|
||||
"token": "firefox@developer-edition",
|
||||
"installer_format": "dmg",
|
||||
"default_categories": ["Browsers"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Mozilla Firefox Nightly",
|
||||
"slug": "firefox@nightly/darwin",
|
||||
"unique_identifier": "org.mozilla.nightly",
|
||||
"token": "firefox@nightly",
|
||||
"installer_format": "dmg",
|
||||
"default_categories": ["Browsers"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Mozilla Firefox Developer Edition",
|
||||
"slug": "firefox@developer-edition/windows",
|
||||
"package_identifier": "Mozilla.Firefox.DeveloperEdition",
|
||||
"unique_identifier": "Firefox Developer Edition (x64 en-US)",
|
||||
"install_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_install.ps1",
|
||||
"uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_uninstall.ps1",
|
||||
"installer_arch": "x64",
|
||||
"installer_type": "exe",
|
||||
"installer_scope": "machine",
|
||||
"default_categories": ["Browsers"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Mozilla Firefox Nightly",
|
||||
"slug": "firefox@nightly/windows",
|
||||
"package_identifier": "Mozilla.Firefox.Nightly.MSIX",
|
||||
"unique_identifier": "Firefox Nightly",
|
||||
"program_publisher": "Mozilla Corporation",
|
||||
"install_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_nightly_install.ps1",
|
||||
"uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_nightly_uninstall.ps1",
|
||||
"installer_arch": "x64",
|
||||
"installer_type": "msix",
|
||||
"installer_scope": "user",
|
||||
"default_categories": ["Browsers"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Learn more about .exe install scripts:
|
||||
# http://fleetdm.com/learn-more-about/exe-install-scripts
|
||||
|
||||
$exeFilePath = "${env:INSTALLER_PATH}"
|
||||
|
||||
try {
|
||||
|
||||
# Firefox's full installer is NSIS-based; /S installs silently and machine-wide.
|
||||
$processOptions = @{
|
||||
FilePath = "$exeFilePath"
|
||||
ArgumentList = "/S"
|
||||
PassThru = $true
|
||||
Wait = $true
|
||||
}
|
||||
|
||||
# Start process and track exit code
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# Prints the exit code
|
||||
Write-Host "Install exit code: $exitCode"
|
||||
Exit $exitCode
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
Exit 1
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID
|
||||
# variable
|
||||
$softwareName = "Firefox Developer Edition"
|
||||
|
||||
# Developer Edition registers as "Firefox Developer Edition (x64 en-US)"; the
|
||||
# prefix match cannot hit the release, ESR, or Nightly entries.
|
||||
$softwareNameLike = "$softwareName*"
|
||||
|
||||
# Firefox's NSIS uninstaller (helper.exe) runs silently with /S.
|
||||
$uninstallArgs = "/S"
|
||||
|
||||
$machineKey = `
|
||||
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
$machineKey32on64 = `
|
||||
'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
|
||||
$exitCode = 0
|
||||
|
||||
try {
|
||||
|
||||
[array]$uninstallKeys = Get-ChildItem `
|
||||
-Path @($machineKey, $machineKey32on64) `
|
||||
-ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-ItemProperty $_.PSPath }
|
||||
|
||||
$foundUninstaller = $false
|
||||
foreach ($key in $uninstallKeys) {
|
||||
# If needed, add -notlike to the comparison to exclude certain similar
|
||||
# software
|
||||
if ($key.DisplayName -like $softwareNameLike) {
|
||||
$foundUninstaller = $true
|
||||
# Get the uninstall command. Some uninstallers do not include
|
||||
# 'QuietUninstallString' and require a flag to run silently.
|
||||
$uninstallCommand = if ($key.QuietUninstallString) {
|
||||
$key.QuietUninstallString
|
||||
} else {
|
||||
$key.UninstallString
|
||||
}
|
||||
|
||||
# The uninstall command may contain command and args, like:
|
||||
# "C:\Program Files\Software\uninstall.exe" --uninstall --silent
|
||||
# Split the command and args
|
||||
$splitArgs = $uninstallCommand.Split('"')
|
||||
if ($splitArgs.Length -gt 1) {
|
||||
if ($splitArgs.Length -eq 3) {
|
||||
$uninstallArgs = "$( $splitArgs[2] ) $uninstallArgs".Trim()
|
||||
} elseif ($splitArgs.Length -gt 3) {
|
||||
Throw `
|
||||
"Uninstall command contains multiple quoted strings. " +
|
||||
"Please update the uninstall script.`n" +
|
||||
"Uninstall command: $uninstallCommand"
|
||||
}
|
||||
$uninstallCommand = $splitArgs[1]
|
||||
}
|
||||
Write-Host "Uninstall command: $uninstallCommand"
|
||||
Write-Host "Uninstall args: $uninstallArgs"
|
||||
|
||||
$processOptions = @{
|
||||
FilePath = $uninstallCommand
|
||||
PassThru = $true
|
||||
Wait = $true
|
||||
}
|
||||
if ($uninstallArgs -ne '') {
|
||||
$processOptions.ArgumentList = "$uninstallArgs"
|
||||
}
|
||||
|
||||
# Start process and track exit code
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# Prints the exit code
|
||||
Write-Host "Uninstall exit code: $exitCode"
|
||||
# Exit the loop once the software is found and uninstalled.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $foundUninstaller) {
|
||||
Write-Host "Uninstaller for '$softwareName' not found."
|
||||
# Change exit code to 0 if you don't want to fail if uninstaller is not
|
||||
# found. This could happen if program was already uninstalled.
|
||||
$exitCode = 1
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
$exitCode = 1
|
||||
}
|
||||
|
||||
Exit $exitCode
|
||||
@@ -0,0 +1,99 @@
|
||||
# MSIX: provision machine-wide so the app is available to all users at sign-in, then
|
||||
# opportunistically register for the currently logged-on console user (via a scheduled
|
||||
# task in their session) so the app is immediately visible without requiring sign-out.
|
||||
#
|
||||
# The Fleet agent runs as Local System on Windows, and Add-AppxPackage cannot run in that
|
||||
# context (HRESULT 0x80073CF9). The scheduled task is the supported way to register a
|
||||
# package in a user session from a system-context script.
|
||||
|
||||
$softwareName = "FirefoxNightly"
|
||||
$taskName = "fleet-install-$softwareName.msix"
|
||||
$scriptPath = "$env:PUBLIC\install-$softwareName.ps1"
|
||||
$exitCodeFile = "$env:PUBLIC\install-exitcode-$softwareName.txt"
|
||||
|
||||
try {
|
||||
|
||||
$msixPath = $env:INSTALLER_PATH
|
||||
if (-not $msixPath) {
|
||||
throw "INSTALLER_PATH is not set"
|
||||
}
|
||||
|
||||
Write-Host "Provisioning MSIX for all users..."
|
||||
$result = Add-AppxProvisionedPackage -Online -PackagePath $msixPath -SkipLicense -Regions "all" -ErrorAction Stop
|
||||
$result | Out-String | Write-Host
|
||||
|
||||
# Win32_ComputerSystem.UserName returns the console user (DOMAIN\User) or null when no
|
||||
# interactive session is active. Other RDP/fast-user-switch sessions won't get the
|
||||
# immediate registration; those users will pick it up from the provisioned install at
|
||||
# their next sign-in.
|
||||
$userName = (Get-CimInstance Win32_ComputerSystem).UserName
|
||||
if (-not $userName -or $userName -notlike "*\*") {
|
||||
Write-Host "No interactive user logged on; provisioned install will register for each user at sign-in."
|
||||
Start-Sleep -Seconds 5
|
||||
Exit 0
|
||||
}
|
||||
|
||||
Write-Host "Registering MSIX for logged-on user '$userName' via scheduled task..."
|
||||
|
||||
$userScript = @"
|
||||
`$msixPath = "$msixPath"
|
||||
`$exitCodeFile = "$exitCodeFile"
|
||||
try {
|
||||
Add-AppxPackage -Path `$msixPath -ErrorAction Stop | Out-String | Write-Host
|
||||
Set-Content -Path `$exitCodeFile -Value 0
|
||||
} catch {
|
||||
Write-Host "Add-AppxPackage failed: `$(`$_.Exception.Message)"
|
||||
Set-Content -Path `$exitCodeFile -Value 1
|
||||
}
|
||||
"@
|
||||
|
||||
Set-Content -Path $scriptPath -Value $userScript -Force
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$scriptPath`""
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userName -RunLevel Highest
|
||||
$task = New-ScheduledTask -Action $action -Settings $settings -Principal $principal
|
||||
Register-ScheduledTask -TaskName $taskName -InputObject $task -User $userName -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
|
||||
$startDate = Get-Date
|
||||
$state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State
|
||||
while ($state -ne "Running") {
|
||||
Start-Sleep -Seconds 1
|
||||
if ((New-Timespan -Start $startDate).TotalSeconds -gt 30) {
|
||||
Write-Host "Per-user registration task did not start within 30s; provisioned install is still valid."
|
||||
break
|
||||
}
|
||||
$state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State
|
||||
}
|
||||
|
||||
while ($state -eq "Running") {
|
||||
Start-Sleep -Seconds 2
|
||||
if ((New-Timespan -Start $startDate).TotalSeconds -gt 90) {
|
||||
Write-Host "Per-user registration task did not complete within 90s; provisioned install is still valid."
|
||||
break
|
||||
}
|
||||
$state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State
|
||||
}
|
||||
|
||||
if (Test-Path $exitCodeFile) {
|
||||
$code = (Get-Content $exitCodeFile -ErrorAction SilentlyContinue | Select-Object -First 1).Trim()
|
||||
if ($code -eq "0") {
|
||||
Write-Host "Per-user registration completed for '$userName'."
|
||||
} else {
|
||||
Write-Host "Per-user registration did not complete cleanly (exit code: $code). Provisioned install is still valid."
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
Exit 0
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
Exit 1
|
||||
} finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
|
||||
Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $exitCodeFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
$timeoutSeconds = 300 # 5 minute timeout
|
||||
|
||||
# Match only the Nightly channel: its MSIX identity "Mozilla.MozillaFirefoxNightly"
|
||||
# cannot collide with other Firefox channels' identities. Don't match on a
|
||||
# PackageFamilyName property: Get-AppxProvisionedPackage doesn't expose it, so an
|
||||
# "-eq" match is $null for every package.
|
||||
function ShouldRemoveFirefoxNightlyPackage {
|
||||
param([Parameter(Mandatory=$true)]$pkg)
|
||||
try {
|
||||
$name = [string]$pkg.Name
|
||||
$family = [string]$pkg.PackageFamilyName
|
||||
|
||||
if ($name -and ($name -like "*MozillaFirefoxNightly*")) { return $true }
|
||||
if ($family -and ($family -like "*MozillaFirefoxNightly*")) { return $true }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$start = Get-Date
|
||||
|
||||
$provisioned = Get-AppxProvisionedPackage -Online -ErrorAction Stop | Where-Object {
|
||||
($_.DisplayName -and ($_.DisplayName -like "*MozillaFirefoxNightly*")) -or
|
||||
($_.PackageName -and ($_.PackageName -like "*MozillaFirefoxNightly*"))
|
||||
}
|
||||
foreach ($pkg in $provisioned) {
|
||||
Write-Host "Removing provisioned package: $($pkg.PackageName)"
|
||||
Remove-AppxProvisionedPackage -Online -PackageName $pkg.PackageName -AllUsers -ErrorAction Stop | Out-String | Write-Host
|
||||
$elapsed = (New-TimeSpan -Start $start).TotalSeconds
|
||||
if ($elapsed -gt $timeoutSeconds) {
|
||||
Exit 1603
|
||||
}
|
||||
}
|
||||
|
||||
$installed = Get-AppxPackage -AllUsers -PackageTypeFilter Main -ErrorAction SilentlyContinue | Where-Object {
|
||||
ShouldRemoveFirefoxNightlyPackage $_
|
||||
}
|
||||
foreach ($app in $installed) {
|
||||
Write-Host "Removing installed package: $($app.PackageFullName)"
|
||||
Remove-AppxPackage -Package $app.PackageFullName -AllUsers -ErrorAction Stop | Out-String | Write-Host
|
||||
$elapsed = (New-TimeSpan -Start $start).TotalSeconds
|
||||
if ($elapsed -gt $timeoutSeconds) {
|
||||
Exit 1603
|
||||
}
|
||||
}
|
||||
|
||||
Exit 0
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
Exit 1603
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID
|
||||
# variable
|
||||
$softwareName = "Firefox"
|
||||
$softwareName = "Mozilla Firefox"
|
||||
|
||||
# It is recommended to use exact software name here if possible to avoid
|
||||
# uninstalling unintended software.
|
||||
$softwareNameLike = "*$softwareName*"
|
||||
# Match only the release channel ("Mozilla Firefox (x64 en-US)"); the prefix
|
||||
# match plus the ESR exclusion below keeps ESR, Developer Edition, and Nightly
|
||||
# entries untouched.
|
||||
$softwareNameLike = "$softwareName*"
|
||||
|
||||
# Some uninstallers require a flag to run silently.
|
||||
# Each uninstaller might use different argument (usually it's "/S" or "/s")
|
||||
@@ -28,7 +29,8 @@ $foundUninstaller = $false
|
||||
foreach ($key in $uninstallKeys) {
|
||||
# If needed, add -notlike to the comparison to exclude certain similar
|
||||
# software
|
||||
if ($key.DisplayName -like $softwareNameLike) {
|
||||
if ($key.DisplayName -like $softwareNameLike -and
|
||||
$key.DisplayName -notlike "*ESR*") {
|
||||
$foundUninstaller = $true
|
||||
# Get the uninstall command. Some uninstallers do not include
|
||||
# 'QuietUninstallString' and require a flag to run silently.
|
||||
|
||||
@@ -3277,6 +3277,20 @@
|
||||
"unique_identifier": "Mozilla Firefox (x64 en-US)",
|
||||
"description": "Firefox is a powerful, open-source web browser built for speed, privacy, and customization."
|
||||
},
|
||||
{
|
||||
"name": "Mozilla Firefox Developer Edition",
|
||||
"slug": "firefox@developer-edition/darwin",
|
||||
"platform": "darwin",
|
||||
"unique_identifier": "org.mozilla.firefoxdeveloperedition",
|
||||
"description": "Mozilla Firefox Developer Edition is the version of the Firefox web browser made for web developers, with cutting-edge features and built-in developer tools."
|
||||
},
|
||||
{
|
||||
"name": "Mozilla Firefox Developer Edition",
|
||||
"slug": "firefox@developer-edition/windows",
|
||||
"platform": "windows",
|
||||
"unique_identifier": "Firefox Developer Edition (x64 en-US)",
|
||||
"description": "Mozilla Firefox Developer Edition is the version of the Firefox web browser made for web developers, with cutting-edge features and built-in developer tools."
|
||||
},
|
||||
{
|
||||
"name": "Mozilla Firefox ESR",
|
||||
"slug": "firefox@esr/darwin",
|
||||
@@ -3291,6 +3305,20 @@
|
||||
"unique_identifier": "Mozilla Firefox 140.7.1 ESR (x64 en-US)",
|
||||
"description": "Mozilla Firefox ESR is the Extended Support Release version of the popular web browser Firefox."
|
||||
},
|
||||
{
|
||||
"name": "Mozilla Firefox Nightly",
|
||||
"slug": "firefox@nightly/darwin",
|
||||
"platform": "darwin",
|
||||
"unique_identifier": "org.mozilla.nightly",
|
||||
"description": "Mozilla Firefox Nightly is the daily development build of the Firefox web browser, with the newest features before they reach beta and release."
|
||||
},
|
||||
{
|
||||
"name": "Mozilla Firefox Nightly",
|
||||
"slug": "firefox@nightly/windows",
|
||||
"platform": "windows",
|
||||
"unique_identifier": "Firefox Nightly",
|
||||
"description": "Mozilla Firefox Nightly is the daily development build of the Firefox web browser, with the newest features before they reach beta and release."
|
||||
},
|
||||
{
|
||||
"name": "Fission",
|
||||
"slug": "fission/darwin",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"installer_url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/152.0.6/win64/en-US/Firefox%20Setup%20152.0.6.exe",
|
||||
"install_script_ref": "80fb9175",
|
||||
"uninstall_script_ref": "8b5e20e4",
|
||||
"uninstall_script_ref": "ae547434",
|
||||
"sha256": "3d4fcc5370bb183c9535d64b98d946c2dacf664a394ebcc02844e361d58d59b4",
|
||||
"default_categories": [
|
||||
"Browsers"
|
||||
@@ -17,6 +17,6 @@
|
||||
],
|
||||
"refs": {
|
||||
"80fb9175": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Add argument to install silently\n# Argument to make install silent depends on installer,\n# each installer might use different argument (usually it's \"/S\" or \"/s\")\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/S\"\n PassThru = $true\n Wait = $true\n}\n \n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Add arguments to install silently (Firefox uses an Inno Setup-based installer)\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /MERGETASKS=!runcode\"\n PassThru = $true\n Wait = $true\n}\n \n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n",
|
||||
"8b5e20e4": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = \"Firefox\"\n\n# It is recommended to use exact software name here if possible to avoid\n# uninstalling unintended software.\n$softwareNameLike = \"*$softwareName*\"\n\n# Some uninstallers require a flag to run silently.\n# Each uninstaller might use different argument (usually it's \"/S\" or \"/s\")\n$uninstallArgs = \"/S\"\n\n$machineKey = `\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = `\n 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($machineKey, $machineKey32on64) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start process and track exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n # Change exit code to 0 if you don't want to fail if uninstaller is not\n # found. This could happen if program was already uninstalled.\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\nExit $exitCode\n"
|
||||
"ae547434": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = \"Mozilla Firefox\"\n\n# Match only the release channel (\"Mozilla Firefox (x64 en-US)\"); the prefix\n# match plus the ESR exclusion below keeps ESR, Developer Edition, and Nightly\n# entries untouched.\n$softwareNameLike = \"$softwareName*\"\n\n# Some uninstallers require a flag to run silently.\n# Each uninstaller might use different argument (usually it's \"/S\" or \"/s\")\n$uninstallArgs = \"/S\"\n\n$machineKey = `\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = `\n 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($machineKey, $machineKey32on64) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike -and\n $key.DisplayName -notlike \"*ESR*\") {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start process and track exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n # Change exit code to 0 if you don't want to fail if uninstaller is not\n # found. This could happen if program was already uninstalled.\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\nExit $exitCode\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "153.0b13",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition' AND version_compare(bundle_version, '15326.7.15') < 0);"
|
||||
},
|
||||
"installer_url": "https://download-installer.cdn.mozilla.net/pub/devedition/releases/153.0b13/mac/en-US/Firefox%20153.0b13.dmg",
|
||||
"install_script_ref": "a313f903",
|
||||
"uninstall_script_ref": "b108b0ff",
|
||||
"sha256": "6d30d4c74a97ed2e0fea2956006a1e0d589a856061291eb8bbde95378cc806fd",
|
||||
"default_categories": [
|
||||
"Browsers"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"a313f903": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'org.mozilla.firefoxdeveloperedition'\nif [ -d \"$APPDIR/Firefox Developer Edition.app\" ]; then\n\tsudo mv \"$APPDIR/Firefox Developer Edition.app\" \"$TMPDIR/Firefox Developer Edition.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Firefox Developer Edition.app\" \"$APPDIR\"\nrelaunch_application 'org.mozilla.firefoxdeveloperedition'\n",
|
||||
"b108b0ff": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nsudo rm -rf \"$APPDIR/Firefox Developer Edition.app\"\nsudo rmdir '~/Library/Application Support/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla/updates'\nsudo rmdir '~/Library/Caches/Mozilla/updates/Applications'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/org.mozilla.firefox.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Mozilla/updates/Applications/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.firefox'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.firefox.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/org.mozilla.firefox.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/org.mozilla.firefox'\n"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "151.0",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM programs WHERE name = 'Firefox Developer Edition (x64 en-US)' AND publisher = 'Mozilla';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Firefox Developer Edition (x64 en-US)' AND publisher = 'Mozilla' AND version_compare(version, '151.0') < 0);"
|
||||
},
|
||||
"installer_url": "https://download-installer.cdn.mozilla.net/pub/devedition/releases/151.0b10/win64/en-US/Firefox%20Setup%20151.0b10.exe",
|
||||
"install_script_ref": "30fd5964",
|
||||
"uninstall_script_ref": "cc59b3f5",
|
||||
"sha256": "ef21f97a29de39f55882368e22189c32c289518e9063acf89d1eb28b12f31023",
|
||||
"default_categories": [
|
||||
"Browsers"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"30fd5964": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Firefox's full installer is NSIS-based; /S installs silently and machine-wide.\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/S\"\n PassThru = $true\n Wait = $true\n}\n\n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n",
|
||||
"cc59b3f5": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = \"Firefox Developer Edition\"\n\n# Developer Edition registers as \"Firefox Developer Edition (x64 en-US)\"; the\n# prefix match cannot hit the release, ESR, or Nightly entries.\n$softwareNameLike = \"$softwareName*\"\n\n# Firefox's NSIS uninstaller (helper.exe) runs silently with /S.\n$uninstallArgs = \"/S\"\n\n$machineKey = `\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = `\n 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($machineKey, $machineKey32on64) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start process and track exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n # Change exit code to 0 if you don't want to fail if uninstaller is not\n # found. This could happen if program was already uninstalled.\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\nExit $exitCode\n"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "154.0a1",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly' AND version_compare(bundle_version, '15426.7.17') < 0);"
|
||||
},
|
||||
"installer_url": "https://ftp.mozilla.org/pub/firefox/nightly/2026/07/2026-07-17-09-27-13-mozilla-central/firefox-154.0a1.en-US.mac.dmg",
|
||||
"install_script_ref": "418c9331",
|
||||
"uninstall_script_ref": "d7c711ad",
|
||||
"sha256": "d2e4b8ce0eb19a9d5c99c86bf0b0c660130e4b3357e056da92844c3ce47bb982",
|
||||
"default_categories": [
|
||||
"Browsers"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"418c9331": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'org.mozilla.nightly'\nif [ -d \"$APPDIR/Firefox Nightly.app\" ]; then\n\tsudo mv \"$APPDIR/Firefox Nightly.app\" \"$TMPDIR/Firefox Nightly.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Firefox Nightly.app\" \"$APPDIR\"\nrelaunch_application 'org.mozilla.nightly'\n",
|
||||
"d7c711ad": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nsudo rm -rf \"$APPDIR/Firefox Nightly.app\"\nsudo rmdir '~/Library/Application Support/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla/updates'\nsudo rmdir '~/Library/Caches/Mozilla/updates/Applications'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/org.mozilla.firefox.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Mozilla/updates/Applications/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.firefox'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.firefox.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/org.mozilla.firefox.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/org.mozilla.firefox'\n"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "154.2607.1709.0",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM programs WHERE name = 'Firefox Nightly' AND publisher = 'Mozilla Corporation';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Firefox Nightly' AND publisher = 'Mozilla Corporation' AND version_compare(version, '154.2607.1709.0') < 0);"
|
||||
},
|
||||
"installer_url": "https://ftp.mozilla.org/pub/firefox/nightly/2026/07/2026-07-17-09-27-13-mozilla-central/firefox-154.0a1.multi.win64.installer.msix",
|
||||
"install_script_ref": "255e7c51",
|
||||
"uninstall_script_ref": "f0d0fed1",
|
||||
"sha256": "b9f11a4edf2929d08c179ea9fc86ccd57a7b7b0b111c3d9742dfb2629a8c6cea",
|
||||
"default_categories": [
|
||||
"Browsers"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"255e7c51": "# MSIX: provision machine-wide so the app is available to all users at sign-in, then\n# opportunistically register for the currently logged-on console user (via a scheduled\n# task in their session) so the app is immediately visible without requiring sign-out.\n#\n# The Fleet agent runs as Local System on Windows, and Add-AppxPackage cannot run in that\n# context (HRESULT 0x80073CF9). The scheduled task is the supported way to register a\n# package in a user session from a system-context script.\n\n$softwareName = \"FirefoxNightly\"\n$taskName = \"fleet-install-$softwareName.msix\"\n$scriptPath = \"$env:PUBLIC\\install-$softwareName.ps1\"\n$exitCodeFile = \"$env:PUBLIC\\install-exitcode-$softwareName.txt\"\n\ntry {\n\n $msixPath = $env:INSTALLER_PATH\n if (-not $msixPath) {\n throw \"INSTALLER_PATH is not set\"\n }\n\n Write-Host \"Provisioning MSIX for all users...\"\n $result = Add-AppxProvisionedPackage -Online -PackagePath $msixPath -SkipLicense -Regions \"all\" -ErrorAction Stop\n $result | Out-String | Write-Host\n\n # Win32_ComputerSystem.UserName returns the console user (DOMAIN\\User) or null when no\n # interactive session is active. Other RDP/fast-user-switch sessions won't get the\n # immediate registration; those users will pick it up from the provisioned install at\n # their next sign-in.\n $userName = (Get-CimInstance Win32_ComputerSystem).UserName\n if (-not $userName -or $userName -notlike \"*\\*\") {\n Write-Host \"No interactive user logged on; provisioned install will register for each user at sign-in.\"\n Start-Sleep -Seconds 5\n Exit 0\n }\n\n Write-Host \"Registering MSIX for logged-on user '$userName' via scheduled task...\"\n\n $userScript = @\"\n`$msixPath = \"$msixPath\"\n`$exitCodeFile = \"$exitCodeFile\"\ntry {\n Add-AppxPackage -Path `$msixPath -ErrorAction Stop | Out-String | Write-Host\n Set-Content -Path `$exitCodeFile -Value 0\n} catch {\n Write-Host \"Add-AppxPackage failed: `$(`$_.Exception.Message)\"\n Set-Content -Path `$exitCodeFile -Value 1\n}\n\"@\n\n Set-Content -Path $scriptPath -Value $userScript -Force\n\n $action = New-ScheduledTaskAction -Execute \"powershell.exe\" `\n -Argument \"-WindowStyle Hidden -ExecutionPolicy Bypass -File `\"$scriptPath`\"\"\n $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n $principal = New-ScheduledTaskPrincipal -UserId $userName -RunLevel Highest\n $task = New-ScheduledTask -Action $action -Settings $settings -Principal $principal\n Register-ScheduledTask -TaskName $taskName -InputObject $task -User $userName -Force | Out-Null\n Start-ScheduledTask -TaskName $taskName\n\n $startDate = Get-Date\n $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State\n while ($state -ne \"Running\") {\n Start-Sleep -Seconds 1\n if ((New-Timespan -Start $startDate).TotalSeconds -gt 30) {\n Write-Host \"Per-user registration task did not start within 30s; provisioned install is still valid.\"\n break\n }\n $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State\n }\n\n while ($state -eq \"Running\") {\n Start-Sleep -Seconds 2\n if ((New-Timespan -Start $startDate).TotalSeconds -gt 90) {\n Write-Host \"Per-user registration task did not complete within 90s; provisioned install is still valid.\"\n break\n }\n $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State\n }\n\n if (Test-Path $exitCodeFile) {\n $code = (Get-Content $exitCodeFile -ErrorAction SilentlyContinue | Select-Object -First 1).Trim()\n if ($code -eq \"0\") {\n Write-Host \"Per-user registration completed for '$userName'.\"\n } else {\n Write-Host \"Per-user registration did not complete cleanly (exit code: $code). Provisioned install is still valid.\"\n }\n }\n\n Start-Sleep -Seconds 5\n Exit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n} finally {\n Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null\n Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue\n Remove-Item -Path $exitCodeFile -Force -ErrorAction SilentlyContinue\n}\n",
|
||||
"f0d0fed1": "$timeoutSeconds = 300 # 5 minute timeout\n\n# Match only the Nightly channel: its MSIX identity \"Mozilla.MozillaFirefoxNightly\"\n# cannot collide with other Firefox channels' identities. Don't match on a\n# PackageFamilyName property: Get-AppxProvisionedPackage doesn't expose it, so an\n# \"-eq\" match is $null for every package.\nfunction ShouldRemoveFirefoxNightlyPackage {\n param([Parameter(Mandatory=$true)]$pkg)\n try {\n $name = [string]$pkg.Name\n $family = [string]$pkg.PackageFamilyName\n\n if ($name -and ($name -like \"*MozillaFirefoxNightly*\")) { return $true }\n if ($family -and ($family -like \"*MozillaFirefoxNightly*\")) { return $true }\n } catch {}\n return $false\n}\n\ntry {\n\n $start = Get-Date\n\n $provisioned = Get-AppxProvisionedPackage -Online -ErrorAction Stop | Where-Object {\n ($_.DisplayName -and ($_.DisplayName -like \"*MozillaFirefoxNightly*\")) -or\n ($_.PackageName -and ($_.PackageName -like \"*MozillaFirefoxNightly*\"))\n }\n foreach ($pkg in $provisioned) {\n Write-Host \"Removing provisioned package: $($pkg.PackageName)\"\n Remove-AppxProvisionedPackage -Online -PackageName $pkg.PackageName -AllUsers -ErrorAction Stop | Out-String | Write-Host\n $elapsed = (New-TimeSpan -Start $start).TotalSeconds\n if ($elapsed -gt $timeoutSeconds) {\n Exit 1603\n }\n }\n\n $installed = Get-AppxPackage -AllUsers -PackageTypeFilter Main -ErrorAction SilentlyContinue | Where-Object {\n ShouldRemoveFirefoxNightlyPackage $_\n }\n foreach ($app in $installed) {\n Write-Host \"Removing installed package: $($app.PackageFullName)\"\n Remove-AppxPackage -Package $app.PackageFullName -AllUsers -ErrorAction Stop | Out-String | Write-Host\n $elapsed = (New-TimeSpan -Start $start).TotalSeconds\n if ($elapsed -gt $timeoutSeconds) {\n Exit 1603\n }\n }\n\n Exit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1603\n}\n"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -382,6 +382,8 @@ import Firealpaca from "./Firealpaca";
|
||||
import FireflyIotaDesktop from "./FireflyIotaDesktop";
|
||||
import FireflyShimmer from "./FireflyShimmer";
|
||||
import Firefox from "./Firefox";
|
||||
import FirefoxDeveloperEdition from "./FirefoxDeveloperEdition";
|
||||
import FirefoxNightly from "./FirefoxNightly";
|
||||
import Fission from "./Fission";
|
||||
import FleetDesktop from "./FleetDesktop";
|
||||
import Flexoptix from "./Flexoptix";
|
||||
@@ -1508,6 +1510,8 @@ export const SOFTWARE_NAME_TO_ICON_MAP = {
|
||||
firefly: FireflyIotaDesktop,
|
||||
"firefly shimmer": FireflyShimmer,
|
||||
firefox: Firefox,
|
||||
"firefox developer edition": FirefoxDeveloperEdition,
|
||||
"firefox nightly": FirefoxNightly,
|
||||
fission: Fission,
|
||||
"fleet desktop": FleetDesktop,
|
||||
"flexoptix app": Flexoptix,
|
||||
@@ -1773,6 +1777,8 @@ export const SOFTWARE_NAME_TO_ICON_MAP = {
|
||||
mos: Mos,
|
||||
"mountain duck": MountainDuck,
|
||||
"mozilla firefox": Firefox,
|
||||
"mozilla firefox developer edition": FirefoxDeveloperEdition,
|
||||
"mozilla firefox nightly": FirefoxNightly,
|
||||
"mozilla vpn": MozillaVpn,
|
||||
mqttx: Mqttx,
|
||||
"mullvad browser": MullvadBrowser,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Reference in New Issue
Block a user