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:
Allen Houchins
2026-07-17 16:05:27 -05:00
committed by GitHub
parent 83efe93c29
commit 0e587dc80c
23 changed files with 843 additions and 11 deletions
+9
View File
@@ -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.
+28
View File
@@ -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