initial
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
name: Update LibreWolf cask
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out tap
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update from latest stable release
|
||||
run: python3 scripts/update_librewolf.py
|
||||
|
||||
- name: Commit update
|
||||
run: |
|
||||
if git diff --quiet -- Casks/librewolf.rb; then
|
||||
echo "LibreWolf is already up to date"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "librewolf-update-bot"
|
||||
git config user.email "librewolf-update-bot@users.noreply.gitea.com"
|
||||
git add Casks/librewolf.rb
|
||||
git commit -m "Update LibreWolf cask"
|
||||
git push
|
||||
@@ -0,0 +1,47 @@
|
||||
cask "librewolf" do
|
||||
arch arm: "arm64", intel: "x86_64"
|
||||
os macos: "macos-#{arch}-package.dmg", linux: "linux-#{arch}-appimage.AppImage"
|
||||
|
||||
version "153.0.3,1"
|
||||
sha256 arm: "cf28a2c610cd8be0f698b52c1fb6e530d17be09fb43aba798e762d8423b35950",
|
||||
intel: "031050b89658c380d91f2658bfae7ff7f1297be4fabc240f45f15ef506d2aa75",
|
||||
arm64_linux: "5a20e87564950dc6edcceba9969206664d4df24f7811c4ef545b6cd217fa0807",
|
||||
x86_64_linux: "85da8f1bca931c4f1a2cea9140dd846da9cf2868afe085f556b0e5d637e8467d"
|
||||
|
||||
on_macos do
|
||||
disable! date: "2026-09-01", because: :fails_gatekeeper_check
|
||||
|
||||
app "LibreWolf.app"
|
||||
command_wrapper "librewolf",
|
||||
executable: "#{appdir}/LibreWolf.app/Contents/MacOS/librewolf"
|
||||
|
||||
zap trash: [
|
||||
"~/.librewolf",
|
||||
"~/Library/Application Support/LibreWolf",
|
||||
"~/Library/Caches/LibreWolf Community",
|
||||
"~/Library/Caches/LibreWolf",
|
||||
"~/Library/Preferences/io.gitlab.librewolf-community.librewolf.plist",
|
||||
"~/Library/Saved Application State/io.gitlab.librewolf-community.librewolf.savedState",
|
||||
]
|
||||
end
|
||||
on_linux do
|
||||
app_image "librewolf-#{version.tr(",", "-")}-linux-#{arch}-appimage.AppImage",
|
||||
target: "LibreWolf.AppImage"
|
||||
end
|
||||
|
||||
url "https://codeberg.org/api/packages/librewolf/generic/librewolf/#{version.tr(",", "-")}/librewolf-#{version.tr(",", "-")}-#{os}"
|
||||
name "LibreWolf"
|
||||
desc "Web browser"
|
||||
homepage "https://librewolf.net/"
|
||||
|
||||
# There can be a notable gap between when a version is tagged and a
|
||||
# corresponding release is created, so we check the "latest" release instead
|
||||
# of the Git tags.
|
||||
livecheck do
|
||||
url "https://codeberg.org/api/v1/repos/librewolf/bsys6/releases/latest"
|
||||
regex(/^v?(\d+(?:[.-]\d+)+)$/i)
|
||||
strategy :json do |json, regex|
|
||||
json["tag_name"]&.[](regex, 1)&.tr("-", ",")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
Copyright © 2013, Paul Hinze & Contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Update the LibreWolf cask from the latest stable bsys6 release."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_CASK = ROOT / "Casks" / "librewolf.rb"
|
||||
# Forgejo API for https://librewolf.dev/librewolf/bsys6/releases.
|
||||
LATEST_RELEASE_API = "https://librewolf.dev/api/v1/repos/librewolf/bsys6/releases/latest"
|
||||
PACKAGE_BASE_URL = "https://codeberg.org/api/packages/librewolf/generic/librewolf"
|
||||
USER_AGENT = "homebrew-librewolf-release-updater/1.0"
|
||||
VERSION_RE = re.compile(r'^ version "([^"]+)"$', re.MULTILINE)
|
||||
SHA256_RE = re.compile(
|
||||
r'^ sha256 arm:\s+"[0-9a-f]{64}",\n'
|
||||
r'\s+intel:\s+"[0-9a-f]{64}",\n'
|
||||
r'\s+arm64_linux:\s+"[0-9a-f]{64}",\n'
|
||||
r'\s+x86_64_linux:\s+"[0-9a-f]{64}"$',
|
||||
re.MULTILINE,
|
||||
)
|
||||
STABLE_TAG_RE = re.compile(
|
||||
r"^(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?-(?P<build>\d+)$"
|
||||
)
|
||||
CHECKSUM_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
||||
ASSET_SUFFIXES = {
|
||||
"arm": "macos-arm64-package.dmg",
|
||||
"intel": "macos-x86_64-package.dmg",
|
||||
"arm64_linux": "linux-arm64-appimage.AppImage",
|
||||
"x86_64_linux": "linux-x86_64-appimage.AppImage",
|
||||
}
|
||||
|
||||
|
||||
def fetch(url: str, limit: int) -> bytes:
|
||||
request = Request(
|
||||
url,
|
||||
headers={"Accept": "application/json", "User-Agent": USER_AGENT},
|
||||
)
|
||||
with urlopen(request, timeout=30) as response:
|
||||
body = response.read(limit + 1)
|
||||
if len(body) > limit:
|
||||
raise ValueError(f"response from {url} exceeds {limit} bytes")
|
||||
return body
|
||||
|
||||
|
||||
def latest_stable_release() -> dict[str, Any]:
|
||||
release = json.loads(fetch(LATEST_RELEASE_API, 2_000_000))
|
||||
if not isinstance(release, dict):
|
||||
raise ValueError("latest release API did not return an object")
|
||||
if release.get("draft") or release.get("prerelease"):
|
||||
raise ValueError("latest release is not stable")
|
||||
|
||||
raw_tag = release.get("tag_name")
|
||||
if not isinstance(raw_tag, str):
|
||||
raise ValueError("latest release has no tag_name")
|
||||
tag = raw_tag.removeprefix("v")
|
||||
if not STABLE_TAG_RE.fullmatch(tag):
|
||||
raise ValueError(f"latest release tag is not a stable version: {raw_tag!r}")
|
||||
release["stable_tag"] = tag
|
||||
return release
|
||||
|
||||
|
||||
def version_key(version: str) -> tuple[int, int, int, int]:
|
||||
normalized = version.removeprefix("v").replace(",", "-")
|
||||
match = STABLE_TAG_RE.fullmatch(normalized)
|
||||
if match is None:
|
||||
raise ValueError(f"invalid cask version: {version!r}")
|
||||
return (
|
||||
int(match["major"]),
|
||||
int(match["minor"]),
|
||||
int(match["patch"] or 0),
|
||||
int(match["build"]),
|
||||
)
|
||||
|
||||
|
||||
def release_checksums(release: dict[str, Any]) -> dict[str, str]:
|
||||
assets = release.get("assets")
|
||||
if not isinstance(assets, list):
|
||||
raise ValueError("latest release has no asset list")
|
||||
|
||||
by_name: dict[str, dict[str, Any]] = {}
|
||||
for asset in assets:
|
||||
if not isinstance(asset, dict) or not isinstance(asset.get("name"), str):
|
||||
continue
|
||||
name = asset["name"]
|
||||
if name in by_name:
|
||||
raise ValueError(f"duplicate release asset: {name}")
|
||||
by_name[name] = asset
|
||||
|
||||
tag = release["stable_tag"]
|
||||
checksums: dict[str, str] = {}
|
||||
for cask_arch, suffix in ASSET_SUFFIXES.items():
|
||||
package_name = f"librewolf-{tag}-{suffix}"
|
||||
if package_name not in by_name:
|
||||
raise ValueError(f"missing release asset: {package_name}")
|
||||
|
||||
checksum_name = f"{package_name}.sha256sum"
|
||||
if checksum_name not in by_name:
|
||||
raise ValueError(f"missing release asset: {checksum_name}")
|
||||
checksum_url = f"{PACKAGE_BASE_URL}/{tag}/{checksum_name}"
|
||||
|
||||
checksum_text = fetch(checksum_url, 4096).decode("ascii").strip()
|
||||
checksum = checksum_text.split(maxsplit=1)[0] if checksum_text else ""
|
||||
if not CHECKSUM_RE.fullmatch(checksum):
|
||||
raise ValueError(f"invalid checksum for {package_name}")
|
||||
checksums[cask_arch] = checksum.lower()
|
||||
|
||||
return checksums
|
||||
|
||||
|
||||
def update_cask(cask_path: Path) -> bool:
|
||||
source = cask_path.read_text(encoding="utf-8")
|
||||
version_match = VERSION_RE.search(source)
|
||||
if version_match is None:
|
||||
raise ValueError(f"could not find version stanza in {cask_path}")
|
||||
|
||||
current_version = version_match.group(1)
|
||||
release = latest_stable_release()
|
||||
latest_tag = release["stable_tag"]
|
||||
latest_key = version_key(latest_tag)
|
||||
current_key = version_key(current_version)
|
||||
comparison = (latest_key > current_key) - (latest_key < current_key)
|
||||
if comparison <= 0:
|
||||
state = "current" if comparison == 0 else "newer than the latest stable release"
|
||||
print(f"LibreWolf {current_version} is {state}; no update needed")
|
||||
return False
|
||||
|
||||
checksums = release_checksums(release)
|
||||
cask_version = latest_tag.replace("-", ",")
|
||||
updated, version_count = VERSION_RE.subn(f' version "{cask_version}"', source, count=1)
|
||||
sha256_stanza = (
|
||||
f' sha256 arm: "{checksums["arm"]}",\n'
|
||||
f' intel: "{checksums["intel"]}",\n'
|
||||
f' arm64_linux: "{checksums["arm64_linux"]}",\n'
|
||||
f' x86_64_linux: "{checksums["x86_64_linux"]}"'
|
||||
)
|
||||
updated, sha256_count = SHA256_RE.subn(sha256_stanza, updated, count=1)
|
||||
if version_count != 1 or sha256_count != 1:
|
||||
raise ValueError(f"could not update version and sha256 stanzas in {cask_path}")
|
||||
|
||||
temporary_path = cask_path.with_suffix(f"{cask_path.suffix}.tmp")
|
||||
temporary_path.write_text(updated, encoding="utf-8")
|
||||
temporary_path.replace(cask_path)
|
||||
print(f"Updated LibreWolf from {current_version} to {cask_version}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--cask", type=Path, default=DEFAULT_CASK)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
update_cask(args.cask)
|
||||
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,91 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts import update_librewolf
|
||||
|
||||
|
||||
class VersionOrderingTest(unittest.TestCase):
|
||||
def test_patch_release_is_newer_than_rebuild_without_patch(self) -> None:
|
||||
self.assertLess(
|
||||
update_librewolf.version_key("153.0-3"),
|
||||
update_librewolf.version_key("153.0.1-1"),
|
||||
)
|
||||
|
||||
def test_cask_and_release_versions_have_same_ordering(self) -> None:
|
||||
self.assertEqual(
|
||||
update_librewolf.version_key("153.0.3,1"),
|
||||
update_librewolf.version_key("153.0.3-1"),
|
||||
)
|
||||
|
||||
|
||||
class StableReleaseTest(unittest.TestCase):
|
||||
@patch.object(update_librewolf, "fetch")
|
||||
def test_prerelease_is_rejected(self, fetch) -> None:
|
||||
fetch.return_value = json.dumps(
|
||||
{
|
||||
"tag_name": "154.0-1",
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
}
|
||||
).encode()
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "not stable"):
|
||||
update_librewolf.latest_stable_release()
|
||||
|
||||
|
||||
class CaskUpdateTest(unittest.TestCase):
|
||||
CASK = '''cask "librewolf" do
|
||||
version "152.0.6,1"
|
||||
sha256 arm: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
intel: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
arm64_linux: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
x86_64_linux: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||
end
|
||||
'''
|
||||
CHECKSUMS = {
|
||||
"arm": "1" * 64,
|
||||
"intel": "2" * 64,
|
||||
"arm64_linux": "3" * 64,
|
||||
"x86_64_linux": "4" * 64,
|
||||
}
|
||||
|
||||
@patch.object(update_librewolf, "release_checksums")
|
||||
@patch.object(update_librewolf, "latest_stable_release")
|
||||
def test_new_stable_release_updates_version_and_all_hashes(
|
||||
self, latest_stable_release, release_checksums
|
||||
) -> None:
|
||||
latest_stable_release.return_value = {"stable_tag": "153.0.3-1"}
|
||||
release_checksums.return_value = self.CHECKSUMS
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cask = Path(directory) / "librewolf.rb"
|
||||
cask.write_text(self.CASK)
|
||||
|
||||
self.assertTrue(update_librewolf.update_cask(cask))
|
||||
updated = cask.read_text()
|
||||
|
||||
self.assertIn('version "153.0.3,1"', updated)
|
||||
for checksum in self.CHECKSUMS.values():
|
||||
self.assertIn(checksum, updated)
|
||||
|
||||
@patch.object(update_librewolf, "release_checksums")
|
||||
@patch.object(update_librewolf, "latest_stable_release")
|
||||
def test_older_release_does_not_downgrade_cask(
|
||||
self, latest_stable_release, release_checksums
|
||||
) -> None:
|
||||
latest_stable_release.return_value = {"stable_tag": "151.0.3-1"}
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cask = Path(directory) / "librewolf.rb"
|
||||
cask.write_text(self.CASK)
|
||||
self.assertFalse(update_librewolf.update_cask(cask))
|
||||
self.assertEqual(cask.read_text(), self.CASK)
|
||||
|
||||
release_checksums.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user