fxcss 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fxcss
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Live-reload, inspect and screenshot-test Firefox userChrome.css themes
5
5
  Author: AdamXweb
6
6
  License: MIT
@@ -29,6 +29,14 @@ Edit your CSS and see it live, click any part of the UI to get its selector,
29
29
  and screenshot-test changes in CI.
30
30
  </p>
31
31
 
32
+ **Three ways in:**
33
+
34
+ | You are… | Start with |
35
+ | --- | --- |
36
+ | **Building a theme** | `fxcss watch` — edit CSS, see it live in ~50ms. `fxcss pick` names any element you click. |
37
+ | **Trying someone's theme** | `fxcss try owner/repo` — test-drive it in a throwaway profile; close the window and nothing remains. |
38
+ | **Maintaining a theme repo** | `fxcss init` — before/after screenshots on every pull request, on macOS, Windows and Linux. |
39
+
32
40
  ## Description
33
41
 
34
42
  Working on a Firefox theme normally means: edit CSS, restart Firefox, squint,
@@ -41,10 +49,10 @@ gives you a live-reload loop, an element picker, and a screenshot differ.
41
49
 
42
50
  Your real Firefox profile is never touched.
43
51
 
44
- ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
52
+ ![Three saved edits in fxcss watch, each recolouring the chrome](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch-loop.gif)
45
53
 
46
- <p align="center"><sub><code>examples/minimal-theme</code>, light and dark — every screenshot in this
47
- README was generated by fxcss itself.</sub></p>
54
+ <p align="center"><sub>Three saves in <code>fxcss watch</code> — the window updates in ~50ms.
55
+ Every image in this README was generated by fxcss itself.</sub></p>
48
56
 
49
57
  ## Requirements
50
58
 
@@ -93,6 +101,7 @@ at it with `--theme /path/to/theme`.
93
101
  | [`watch`](#fxcss-watch) | Edit CSS and see it live, no restart |
94
102
  | [`pick`](#fxcss-pick) | Click any part of the UI to get its CSS selector |
95
103
  | [`inspect`](#fxcss-inspect) | Look up a selector you already have |
104
+ | [`init`](#fxcss-init) | Add PR previews and CI checks to your theme repo |
96
105
  | [`audit`](#fxcss-audit) | Find every selector that no longer matches, and suggest fixes |
97
106
  | [`changelog`](#fxcss-changelog) | Diff two Firefox builds to see what chrome changed |
98
107
  | [`snapshot`](#fxcss-changelog) | Record a Firefox's chrome names, to diff against later |
@@ -171,6 +180,8 @@ a file in your editor and the running window updates in about 50ms.
171
180
  The window is yours to drive — open menus, resize it, type in the address bar,
172
181
  right-click things. Nothing is scripted.
173
182
 
183
+ ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
184
+
174
185
  | flag | effect |
175
186
  | --- | --- |
176
187
  | `--dark` | start in dark mode, for testing `prefers-color-scheme` rules |
@@ -233,6 +244,32 @@ styled `#urlbar-background` by id, which many older themes still do. The id was
233
244
  replaced by a class, so the rule silently did nothing and the address bar
234
245
  rendered unstyled. One command found it; the fix was `.urlbar-background`.
235
246
 
247
+ ### fxcss init
248
+
249
+ ```bash
250
+ fxcss init # before/after previews on every PR
251
+ fxcss init --watch --showcase # plus the weekly Firefox audit and
252
+ # release screenshots
253
+ ```
254
+
255
+ **Turn any theme repository into one with CI.** Run it from your theme's root
256
+ and it writes the preview workflows into `.github/workflows/`, ready to commit:
257
+ every pull request then gets a comment showing the browser chrome before and
258
+ after the change, with changed pixels highlighted — rendered on macOS, Windows
259
+ and Linux, across sixteen views and every variant stylesheet you ship.
260
+
261
+ The generation is the point, not a convenience: the fxcss version is pinned to
262
+ the one doing the generating, and the publish allowlist is enumerated from
263
+ *your* theme's variant folder — the two things that had to be hand-edited, and
264
+ the second one silently drops views when forgotten. Existing files are never
265
+ overwritten without `--force`, and the output tells you the things that
266
+ otherwise surprise people (the comment starts after the workflows reach your
267
+ default branch; first-time contributors need one approval click).
268
+
269
+ If you'd like people to know:
270
+
271
+ [![theme previews by fxcss](https://img.shields.io/badge/theme%20previews-fxcss-ff7139)](https://github.com/AdamXweb/fxcss)
272
+
236
273
  ### fxcss audit
237
274
 
238
275
  ```bash
@@ -480,11 +517,12 @@ Two things to know before wiring this up:
480
517
  posted as a comment, build the images in the `pull_request` job (no write
481
518
  permissions, no secrets) and publish from a separate `workflow_run` job.
482
519
 
483
- `examples/` has working workflows to copy — the pull request preview pair, the
484
- scheduled Firefox-channel watcher, and the showcase capture. See
485
- [`examples/README.md`](examples/README.md) for which to start with and why the
486
- preview is split into two files. This repo's own `.github/workflows/ci.yml`
487
- runs the whole thing against `examples/minimal-theme` on macOS and Windows.
520
+ Don't copy workflow files by hand — `fxcss init` generates them for your theme,
521
+ allowlist and version pin included. [`examples/README.md`](examples/README.md)
522
+ explains the shape of what it writes, most importantly why the preview is two
523
+ workflows (fork PRs get a read-only token, so the half that runs their code
524
+ cannot be the half that posts the comment). This repo's own CI runs the full
525
+ pipeline against `examples/minimal-theme` on macOS, Windows and Linux.
488
526
 
489
527
  ## Things worth knowing
490
528
 
@@ -8,6 +8,14 @@ Edit your CSS and see it live, click any part of the UI to get its selector,
8
8
  and screenshot-test changes in CI.
9
9
  </p>
10
10
 
11
+ **Three ways in:**
12
+
13
+ | You are… | Start with |
14
+ | --- | --- |
15
+ | **Building a theme** | `fxcss watch` — edit CSS, see it live in ~50ms. `fxcss pick` names any element you click. |
16
+ | **Trying someone's theme** | `fxcss try owner/repo` — test-drive it in a throwaway profile; close the window and nothing remains. |
17
+ | **Maintaining a theme repo** | `fxcss init` — before/after screenshots on every pull request, on macOS, Windows and Linux. |
18
+
11
19
  ## Description
12
20
 
13
21
  Working on a Firefox theme normally means: edit CSS, restart Firefox, squint,
@@ -20,10 +28,10 @@ gives you a live-reload loop, an element picker, and a screenshot differ.
20
28
 
21
29
  Your real Firefox profile is never touched.
22
30
 
23
- ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
31
+ ![Three saved edits in fxcss watch, each recolouring the chrome](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch-loop.gif)
24
32
 
25
- <p align="center"><sub><code>examples/minimal-theme</code>, light and dark — every screenshot in this
26
- README was generated by fxcss itself.</sub></p>
33
+ <p align="center"><sub>Three saves in <code>fxcss watch</code> — the window updates in ~50ms.
34
+ Every image in this README was generated by fxcss itself.</sub></p>
27
35
 
28
36
  ## Requirements
29
37
 
@@ -72,6 +80,7 @@ at it with `--theme /path/to/theme`.
72
80
  | [`watch`](#fxcss-watch) | Edit CSS and see it live, no restart |
73
81
  | [`pick`](#fxcss-pick) | Click any part of the UI to get its CSS selector |
74
82
  | [`inspect`](#fxcss-inspect) | Look up a selector you already have |
83
+ | [`init`](#fxcss-init) | Add PR previews and CI checks to your theme repo |
75
84
  | [`audit`](#fxcss-audit) | Find every selector that no longer matches, and suggest fixes |
76
85
  | [`changelog`](#fxcss-changelog) | Diff two Firefox builds to see what chrome changed |
77
86
  | [`snapshot`](#fxcss-changelog) | Record a Firefox's chrome names, to diff against later |
@@ -150,6 +159,8 @@ a file in your editor and the running window updates in about 50ms.
150
159
  The window is yours to drive — open menus, resize it, type in the address bar,
151
160
  right-click things. Nothing is scripted.
152
161
 
162
+ ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
163
+
153
164
  | flag | effect |
154
165
  | --- | --- |
155
166
  | `--dark` | start in dark mode, for testing `prefers-color-scheme` rules |
@@ -212,6 +223,32 @@ styled `#urlbar-background` by id, which many older themes still do. The id was
212
223
  replaced by a class, so the rule silently did nothing and the address bar
213
224
  rendered unstyled. One command found it; the fix was `.urlbar-background`.
214
225
 
226
+ ### fxcss init
227
+
228
+ ```bash
229
+ fxcss init # before/after previews on every PR
230
+ fxcss init --watch --showcase # plus the weekly Firefox audit and
231
+ # release screenshots
232
+ ```
233
+
234
+ **Turn any theme repository into one with CI.** Run it from your theme's root
235
+ and it writes the preview workflows into `.github/workflows/`, ready to commit:
236
+ every pull request then gets a comment showing the browser chrome before and
237
+ after the change, with changed pixels highlighted — rendered on macOS, Windows
238
+ and Linux, across sixteen views and every variant stylesheet you ship.
239
+
240
+ The generation is the point, not a convenience: the fxcss version is pinned to
241
+ the one doing the generating, and the publish allowlist is enumerated from
242
+ *your* theme's variant folder — the two things that had to be hand-edited, and
243
+ the second one silently drops views when forgotten. Existing files are never
244
+ overwritten without `--force`, and the output tells you the things that
245
+ otherwise surprise people (the comment starts after the workflows reach your
246
+ default branch; first-time contributors need one approval click).
247
+
248
+ If you'd like people to know:
249
+
250
+ [![theme previews by fxcss](https://img.shields.io/badge/theme%20previews-fxcss-ff7139)](https://github.com/AdamXweb/fxcss)
251
+
215
252
  ### fxcss audit
216
253
 
217
254
  ```bash
@@ -459,11 +496,12 @@ Two things to know before wiring this up:
459
496
  posted as a comment, build the images in the `pull_request` job (no write
460
497
  permissions, no secrets) and publish from a separate `workflow_run` job.
461
498
 
462
- `examples/` has working workflows to copy — the pull request preview pair, the
463
- scheduled Firefox-channel watcher, and the showcase capture. See
464
- [`examples/README.md`](examples/README.md) for which to start with and why the
465
- preview is split into two files. This repo's own `.github/workflows/ci.yml`
466
- runs the whole thing against `examples/minimal-theme` on macOS and Windows.
499
+ Don't copy workflow files by hand — `fxcss init` generates them for your theme,
500
+ allowlist and version pin included. [`examples/README.md`](examples/README.md)
501
+ explains the shape of what it writes, most importantly why the preview is two
502
+ workflows (fork PRs get a read-only token, so the half that runs their code
503
+ cannot be the half that posts the comment). This repo's own CI runs the full
504
+ pipeline against `examples/minimal-theme` on macOS, Windows and Linux.
467
505
 
468
506
  ## Things worth knowing
469
507
 
@@ -1,3 +1,3 @@
1
1
  """fxcss — a testing toolkit for Firefox userChrome.css themes."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.8.0"
@@ -2,6 +2,7 @@
2
2
  """fxcss - a testing toolkit for Firefox userChrome.css themes.
3
3
 
4
4
  fxcss try download a theme from GitHub and test-drive it
5
+ fxcss init add PR previews and CI checks to your theme repo
5
6
  fxcss watch edit CSS and see it live, no restart
6
7
  fxcss pick click any part of the UI to get its CSS selector
7
8
  fxcss inspect look up a selector you already have
@@ -26,6 +27,22 @@ from pathlib import Path
26
27
  from . import core
27
28
  from . import __version__
28
29
 
30
+ LANDING = """fxcss - a testing toolkit for Firefox userChrome.css themes
31
+
32
+ Building a theme?
33
+ fxcss watch edit CSS, see it live, no restart
34
+ fxcss pick click any part of the UI to get its selector
35
+
36
+ Trying someone else's theme?
37
+ fxcss try owner/repo test-drive it in a throwaway profile
38
+
39
+ Maintaining a theme repository?
40
+ fxcss init add before/after PR previews and CI checks
41
+ fxcss audit find selectors Firefox has renamed
42
+
43
+ Run `fxcss --help` for the full command list, or `fxcss <command> --help`
44
+ for one command. Nothing here ever touches your real Firefox profile."""
45
+
29
46
  TOOLBOX_KEY = {
30
47
  "darwin": "Cmd+Opt+Shift+I",
31
48
  }.get(sys.platform, "Ctrl+Alt+Shift+I")
@@ -159,6 +176,20 @@ def cmd_try(args):
159
176
  shutil.rmtree(workdir, ignore_errors=True)
160
177
 
161
178
 
179
+ def cmd_init(args):
180
+ from . import scaffold
181
+ theme = args.theme.resolve()
182
+ if not (theme / "chrome" / "userChrome.css").exists():
183
+ print(f"error: no chrome/userChrome.css under {theme} - run this from "
184
+ f"your theme's root, or pass --theme", file=sys.stderr)
185
+ return 2
186
+ slugs = sorted(core.find_variant_sheets(theme))
187
+ written, skipped = scaffold.write_workflows(
188
+ theme, slugs, watch=args.watch, showcase=args.showcase, force=args.force)
189
+ print(scaffold.next_steps(written, skipped, slugs, args.watch, args.showcase))
190
+ return 0
191
+
192
+
162
193
  def cmd_watch(args):
163
194
  theme = args.theme.resolve()
164
195
  firefox = core.find_firefox(args.firefox)
@@ -442,6 +473,8 @@ def cmd_doctor(args):
442
473
  print(f"\ntheme: {len(sheets)} stylesheets, {total:,} bytes")
443
474
  missing = [p for p in ("chrome/userChrome.css",) if not (theme / p).exists()]
444
475
  print("missing expected files: " + (", ".join(missing) if missing else "none"))
476
+ print("\nnew here? `fxcss try owner/repo` test-drives a theme; `fxcss init`")
477
+ print("adds PR previews to yours.")
445
478
  return 0
446
479
 
447
480
 
@@ -468,7 +501,7 @@ def main(argv=None):
468
501
  prog="fxcss", description=__doc__,
469
502
  formatter_class=argparse.RawDescriptionHelpFormatter)
470
503
  ap.add_argument("--version", action="version", version=f"fxcss {__version__}")
471
- sub = ap.add_subparsers(dest="cmd", required=True)
504
+ sub = ap.add_subparsers(dest="cmd", required=False)
472
505
 
473
506
  for verb, helptext in (("try", "download a theme from GitHub and test-drive it"),
474
507
  ("install", "alias for try")):
@@ -492,6 +525,17 @@ def main(argv=None):
492
525
  _menus(tr)
493
526
  tr.set_defaults(func=cmd_try)
494
527
 
528
+ ini = sub.add_parser("init", help="add PR previews and CI checks to your theme repo")
529
+ ini.add_argument("--theme", type=Path, default=Path.cwd(),
530
+ help="theme root, the folder containing chrome/ (default: cwd)")
531
+ ini.add_argument("--watch", action="store_true",
532
+ help="also add the weekly Firefox release/beta/nightly audit")
533
+ ini.add_argument("--showcase", action="store_true",
534
+ help="also add the on-release showcase screenshot workflow")
535
+ ini.add_argument("--force", action="store_true",
536
+ help="replace workflow files that already exist")
537
+ ini.set_defaults(func=cmd_init)
538
+
495
539
  w = sub.add_parser("watch", help="live-reload the theme as you edit")
496
540
  _common(w); _menus(w)
497
541
  w.add_argument("--dark", action="store_true", help="start in dark mode")
@@ -577,6 +621,12 @@ def main(argv=None):
577
621
  d.set_defaults(func=cmd_doctor)
578
622
 
579
623
  args = ap.parse_args(argv)
624
+ if args.cmd is None:
625
+ # Bare `fxcss` used to be an argparse error. Greet by task instead:
626
+ # the three reasons someone installs this are not obvious from a
627
+ # subcommand list.
628
+ print(LANDING)
629
+ return 0
580
630
  # Long-running commands are a live log; keep them readable when piped.
581
631
  try:
582
632
  sys.stdout.reconfigure(line_buffering=True)
@@ -73,9 +73,19 @@ def normalise(im, width=PANEL_WIDTH):
73
73
 
74
74
 
75
75
  def diff_stats(base, head):
76
- """Return (changed_pixel_count, total, mask) ignoring sub-threshold noise."""
77
- delta = ImageChops.difference(base, head).convert("L")
78
- mask = delta.point(lambda v: 255 if v >= NOISE_THRESHOLD else 0)
76
+ """Return (changed_pixel_count, total, mask) ignoring sub-threshold noise.
77
+
78
+ Thresholds the per-channel *maximum* difference, not the luminance of the
79
+ difference. Luminance averages the channels, so a chroma-only shift -- a
80
+ toolbar tinted from blue-grey to peach, say -- can slide under the
81
+ threshold while being plainly visible. Recolouring is exactly the kind of
82
+ change a theme differ exists to catch.
83
+ """
84
+ delta = ImageChops.difference(base, head)
85
+ channel_max = delta.split()[0]
86
+ for channel in delta.split()[1:]:
87
+ channel_max = ImageChops.lighter(channel_max, channel)
88
+ mask = channel_max.point(lambda v: 255 if v >= NOISE_THRESHOLD else 0)
79
89
  changed = mask.histogram()[255]
80
90
  return changed, base.width * base.height, mask
81
91
 
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ """Generate CI workflows for a theme repository: the machinery behind
3
+ `fxcss init`.
4
+
5
+ The templates ship inside the package, so a `pipx install fxcss` user gets
6
+ them without ever visiting the repository. Three things are substituted per
7
+ theme, which is exactly the hand-editing that used to trip people up:
8
+
9
+ * the fxcss version pin, set to the version doing the generating
10
+ * the publish allowlist's variant names, enumerated from the theme's own
11
+ optional stylesheets -- an allowlist someone has to remember to extend is
12
+ an allowlist that silently drops views
13
+ * the showcase URL, taken from the repository's origin remote
14
+ """
15
+
16
+ import re
17
+ import subprocess
18
+ from importlib import resources
19
+ from pathlib import Path
20
+
21
+ from . import __version__
22
+
23
+ PREVIEW_FILES = ("pr-preview.yml", "pr-preview-publish.yml", "pr-preview-cleanup.yml")
24
+ WATCH_FILE = "firefox-watch.yml"
25
+ SHOWCASE_FILE = "showcase.yml"
26
+
27
+
28
+ def variant_alternation(slugs):
29
+ """The regex fragment that admits this theme's variant captures.
30
+
31
+ Empty when there are no variants, so the allowlist stays as tight as the
32
+ theme is simple.
33
+ """
34
+ names = sorted(s for s in slugs if re.fullmatch(r"[a-z0-9-]+", s))
35
+ if not names:
36
+ return ""
37
+ return "|variant-(?:" + "|".join(names) + ")"
38
+
39
+
40
+ def https_repo_url(remote):
41
+ """Normalise a git remote to a browsable https URL, or None."""
42
+ if not remote:
43
+ return None
44
+ remote = remote.strip()
45
+ match = re.match(r"^git@([^:]+):(.+?)(?:\.git)?$", remote)
46
+ if match:
47
+ return f"https://{match.group(1)}/{match.group(2)}"
48
+ match = re.match(r"^(https?://[^ ]+?)(?:\.git)?/?$", remote)
49
+ if match:
50
+ return match.group(1)
51
+ return None
52
+
53
+
54
+ def detect_repo_url(theme: Path):
55
+ try:
56
+ raw = subprocess.run(
57
+ ["git", "-C", str(theme), "config", "--get", "remote.origin.url"],
58
+ capture_output=True, text=True, timeout=10).stdout
59
+ except (OSError, subprocess.SubprocessError):
60
+ raw = ""
61
+ return https_repo_url(raw)
62
+
63
+
64
+ def render_template(text, version, variant_alt, repo_url):
65
+ text = text.replace("__FXCSS_VERSION__", version)
66
+ text = text.replace("__FXCSS_VARIANT_ALT__", variant_alt)
67
+ text = text.replace("__FXCSS_REPO_URL__", repo_url or "https://example.com")
68
+ return text
69
+
70
+
71
+ def _template(name):
72
+ return (resources.files("fxcss") / "templates" / name).read_text(encoding="utf-8")
73
+
74
+
75
+ def write_workflows(theme: Path, variant_slugs, watch=False, showcase=False,
76
+ force=False, version=None):
77
+ """Write the chosen workflows into <theme>/.github/workflows.
78
+
79
+ Returns (written, skipped) lists of paths relative to the theme. Existing
80
+ files are never overwritten unless force is set -- a repo's workflows may
81
+ carry local edits, and clobbering them silently would be worse than making
82
+ someone pass --force.
83
+ """
84
+ version = version or f"v{__version__}"
85
+ alt = variant_alternation(variant_slugs)
86
+ repo_url = detect_repo_url(theme)
87
+
88
+ wanted = list(PREVIEW_FILES)
89
+ if watch:
90
+ wanted.append(WATCH_FILE)
91
+ if showcase:
92
+ wanted.append(SHOWCASE_FILE)
93
+
94
+ outdir = theme / ".github" / "workflows"
95
+ outdir.mkdir(parents=True, exist_ok=True)
96
+
97
+ written, skipped = [], []
98
+ for name in wanted:
99
+ target = outdir / name
100
+ rel = target.relative_to(theme)
101
+ if target.exists() and not force:
102
+ skipped.append(rel)
103
+ continue
104
+ target.write_text(render_template(_template(name), version, alt, repo_url),
105
+ encoding="utf-8")
106
+ written.append(rel)
107
+ return written, skipped
108
+
109
+
110
+ BADGE = ("[![theme previews by fxcss]"
111
+ "(https://img.shields.io/badge/theme%20previews-fxcss-ff7139)]"
112
+ "(https://github.com/AdamXweb/fxcss)")
113
+
114
+
115
+ def next_steps(written, skipped, variant_slugs, watch, showcase):
116
+ """Human instructions for what was generated. Returned, not printed, so it
117
+ is unit-testable and the CLI owns all output."""
118
+ lines = []
119
+ if written:
120
+ lines.append("Wrote:")
121
+ lines += [f" {p}" for p in written]
122
+ if skipped:
123
+ lines.append("Left alone (already exist; rerun with --force to replace):")
124
+ lines += [f" {p}" for p in skipped]
125
+ if variant_slugs:
126
+ lines.append("")
127
+ lines.append(f"Publish allowlist covers {len(variant_slugs)} variant "
128
+ f"stylesheet(s): {', '.join(sorted(variant_slugs))}.")
129
+ lines.append("Adding a variant later? Re-run `fxcss init --force`, or add it "
130
+ "to the NAME regex by hand.")
131
+ lines += [
132
+ "",
133
+ "Worth knowing before the first pull request:",
134
+ " - The preview comment only starts appearing once these files are on",
135
+ " your default branch: workflow_run always uses the default-branch copy.",
136
+ " - A first-time contributor's run waits for your 'Approve and run",
137
+ " workflows' click. That is GitHub policy, not a misconfiguration.",
138
+ " - Images are published to an orphan ci-previews branch and cleaned up",
139
+ " when each pull request closes.",
140
+ ]
141
+ if watch:
142
+ lines.append(" - firefox-watch runs Mondays and needs no further setup.")
143
+ if showcase:
144
+ lines.append(" - showcase publishes to a `showcase` branch on each release.")
145
+ lines += [
146
+ "",
147
+ "If you would like to say so in your README:",
148
+ f" {BADGE}",
149
+ ]
150
+ return "\n".join(lines)
@@ -0,0 +1,180 @@
1
+ name: Firefox watch
2
+
3
+ # Firefox ships every few weeks and quietly renames parts of its UI. A theme
4
+ # does not break loudly when that happens -- the rule simply stops matching and
5
+ # that piece of styling disappears, with nothing logged anywhere.
6
+ #
7
+ # This runs fxcss audit against each Firefox channel on a schedule. Beta and
8
+ # Nightly are the point of the exercise: they show what is coming weeks before
9
+ # it reaches the people using the theme.
10
+ #
11
+ # When there are confident fixes it opens a pull request with them. When there
12
+ # are findings it cannot fix for you, it opens (or updates) one issue per
13
+ # channel instead, and closes it again once the channel comes back clean.
14
+
15
+ on:
16
+ schedule:
17
+ # Mondays, 06:20 UTC. Off the hour because scheduled jobs bunch there.
18
+ - cron: '20 6 * * 1'
19
+ workflow_dispatch:
20
+ inputs:
21
+ channel:
22
+ description: 'Single channel to check (blank = all)'
23
+ required: false
24
+
25
+ permissions:
26
+ contents: write
27
+ pull-requests: write
28
+ issues: write
29
+
30
+ env:
31
+ FXCSS_VERSION: __FXCSS_VERSION__
32
+
33
+ jobs:
34
+ check:
35
+ name: ${{ matrix.channel }}
36
+ runs-on: macos-latest
37
+ timeout-minutes: 30
38
+ strategy:
39
+ fail-fast: false
40
+ matrix:
41
+ channel: [release, beta, nightly]
42
+
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - uses: actions/setup-python@v5
47
+ with:
48
+ python-version: '3.12'
49
+
50
+ - name: Install fxcss
51
+ run: >-
52
+ python -m pip install --disable-pip-version-check
53
+ "fxcss[images] @ git+https://github.com/AdamXweb/fxcss@${{ env.FXCSS_VERSION }}"
54
+
55
+ - name: Install Firefox (${{ matrix.channel }})
56
+ if: ${{ github.event.inputs.channel == '' || github.event.inputs.channel == matrix.channel }}
57
+ run: |
58
+ set -euo pipefail
59
+ case "${{ matrix.channel }}" in
60
+ release) product=firefox-latest-ssl ;;
61
+ beta) product=firefox-beta-latest-ssl ;;
62
+ nightly) product=firefox-nightly-latest-ssl ;;
63
+ esr) product=firefox-esr-latest-ssl ;;
64
+ *) echo "unknown channel"; exit 1 ;;
65
+ esac
66
+ curl -sSL -o firefox.dmg \
67
+ "https://download.mozilla.org/?product=${product}&os=osx&lang=en-US"
68
+ hdiutil attach -nobrowse -quiet -mountpoint /Volumes/ffwatch firefox.dmg
69
+ cp -R /Volumes/ffwatch/*.app "$RUNNER_TEMP/Firefox.app"
70
+ hdiutil detach /Volumes/ffwatch -quiet
71
+ echo "FIREFOX_BIN=$RUNNER_TEMP/Firefox.app/Contents/MacOS/firefox" >> "$GITHUB_ENV"
72
+
73
+ - name: Audit
74
+ id: audit
75
+ if: ${{ github.event.inputs.channel == '' || github.event.inputs.channel == matrix.channel }}
76
+ run: |
77
+ set +e
78
+ # Note the redirect rather than a pipe into tee: after a pipeline $?
79
+ # is tee's status, which is always 0, so findings would never be seen.
80
+ fxcss audit --no-colour --no-unused --strict --patch fxcss-fix.patch > audit.txt 2>&1
81
+ status=$?
82
+ cat audit.txt
83
+ echo "status=$status" >> "$GITHUB_OUTPUT"
84
+
85
+ version=$(fxcss doctor 2>/dev/null \
86
+ | sed -n 's/^version: *\([0-9][^ ]*\).*/\1/p' | head -1)
87
+ echo "version=${version:-unknown}" >> "$GITHUB_OUTPUT"
88
+ if [ -s fxcss-fix.patch ]; then echo "patch=yes" >> "$GITHUB_OUTPUT"; fi
89
+
90
+ - name: Open a pull request with the confident fixes
91
+ if: steps.audit.outputs.status != '0' && steps.audit.outputs.patch == 'yes'
92
+ env:
93
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94
+ CHANNEL: ${{ matrix.channel }}
95
+ VERSION: ${{ steps.audit.outputs.version }}
96
+ run: |
97
+ set -euo pipefail
98
+ branch="fxcss/firefox-${CHANNEL}-${VERSION}"
99
+ if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
100
+ echo "branch $branch already exists; nothing new to open"
101
+ exit 0
102
+ fi
103
+ git config user.name 'github-actions[bot]'
104
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
105
+ git checkout -b "$branch"
106
+ git apply fxcss-fix.patch
107
+ git add -A ':!audit.txt' ':!fxcss-fix.patch'
108
+ git commit -m "fix(css): update selectors renamed in Firefox ${VERSION}"
109
+ git push origin "$branch"
110
+ {
111
+ echo "Firefox **${CHANNEL}** is now **${VERSION}**, and some selectors this"
112
+ echo "theme uses no longer match anything in it."
113
+ echo
114
+ echo "The changes here are the ones fxcss is certain about — a name that moved"
115
+ echo "between an id and a class. Anything it was less sure of is listed below"
116
+ echo "but **not** changed."
117
+ echo
118
+ echo '```'
119
+ cat audit.txt
120
+ echo '```'
121
+ echo
122
+ echo "Worth previewing before merging: the PR preview workflow will render"
123
+ echo "before and after on this pull request."
124
+ } > body.md
125
+ gh pr create --base "${GITHUB_REF_NAME}" --head "$branch" \
126
+ --title "Firefox ${CHANNEL} ${VERSION}: update renamed selectors" \
127
+ --body-file body.md
128
+
129
+ - name: Open or update the heads-up issue
130
+ if: steps.audit.outputs.status != '0' && steps.audit.outputs.patch != 'yes'
131
+ env:
132
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
133
+ CHANNEL: ${{ matrix.channel }}
134
+ VERSION: ${{ steps.audit.outputs.version }}
135
+ run: |
136
+ set -euo pipefail
137
+ title="Firefox ${CHANNEL}: selectors need attention"
138
+ {
139
+ echo "Firefox **${CHANNEL}** is now **${VERSION}**. Some selectors this theme"
140
+ echo "uses no longer match anything, and fxcss was not confident enough to"
141
+ echo "propose a replacement automatically."
142
+ echo
143
+ echo '```'
144
+ cat audit.txt
145
+ echo '```'
146
+ echo
147
+ echo "_Updated automatically. This closes itself when the channel is clean._"
148
+ } > body.md
149
+ existing=$(gh issue list --state open --search "$title in:title" \
150
+ --json number,title --jq "[.[] | select(.title == \"$title\")][0].number")
151
+ if [ -n "$existing" ] && [ "$existing" != "null" ]; then
152
+ gh issue edit "$existing" --body-file body.md
153
+ else
154
+ gh issue create --title "$title" --body-file body.md
155
+ fi
156
+
157
+ - name: Close the issue if this channel is clean again
158
+ if: steps.audit.outputs.status == '0'
159
+ env:
160
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
161
+ CHANNEL: ${{ matrix.channel }}
162
+ run: |
163
+ set -euo pipefail
164
+ title="Firefox ${CHANNEL}: selectors need attention"
165
+ existing=$(gh issue list --state open --search "$title in:title" \
166
+ --json number,title --jq "[.[] | select(.title == \"$title\")][0].number")
167
+ if [ -n "$existing" ] && [ "$existing" != "null" ]; then
168
+ gh issue close "$existing" \
169
+ --comment "Firefox ${CHANNEL} audits clean again — closing."
170
+ fi
171
+
172
+ - uses: actions/upload-artifact@v4
173
+ if: always()
174
+ with:
175
+ name: audit-${{ matrix.channel }}
176
+ path: |
177
+ audit.txt
178
+ fxcss-fix.patch
179
+ if-no-files-found: ignore
180
+ retention-days: 30